feat: add brain MCP tools and webview service
Adds brain_remember/recall/forget/ensure_collection MCP tools via BrainService. Extracts WebviewService, wires brain tools into both headless and GUI MCP bridges. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
e2c6a79487
commit
5a2d508254
8 changed files with 735 additions and 35 deletions
74
CLAUDE.md
Normal file
74
CLAUDE.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
```bash
|
||||
# Development (hot-reload GUI + Go rebuild)
|
||||
wails3 dev
|
||||
|
||||
# Production build (preferred)
|
||||
core build
|
||||
|
||||
# Frontend-only development
|
||||
cd frontend && npm install && npm run dev
|
||||
|
||||
# Go tests
|
||||
core go test # All tests
|
||||
core go test --run TestName # Single test
|
||||
core go cov # Coverage report
|
||||
core go cov --open # Coverage in browser
|
||||
|
||||
# Quality assurance
|
||||
core go qa # Format + vet + lint + test
|
||||
core go qa full # + race detector, vuln scan, security audit
|
||||
core go fmt # Format only
|
||||
core go lint # Lint only
|
||||
|
||||
# Frontend tests
|
||||
cd frontend && npm run test
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
**Dual-mode application**: a single Go binary that operates as either a GUI desktop app (Wails 3 + Angular) or a headless CI/CD daemon, determined at startup by the `--headless` flag or display availability.
|
||||
|
||||
### GUI Mode (default)
|
||||
`main()` → Wails application with embedded Angular frontend, system tray (macOS: accessory app, no Dock icon), and an MCP HTTP server on **port 9877**. The `MCPBridge` service orchestrates `WebviewService` (window/DOM automation), `BrainService` (vector knowledge store via OpenBrain), and a WebSocket hub.
|
||||
|
||||
### Headless Mode (`--headless` or no display)
|
||||
`startHeadless()` → Daemon with PID file at `~/.core/core-ide.pid`, health check on **port 9878**, and a minimal MCP server on **port 9877**. Polls Forgejo repos (configured via `CORE_REPOS` env var) every 60 seconds and runs jobs through an ordered handler pipeline: PublishDraft → SendFix → DismissReviews → EnableAutoMerge → TickParent → Dispatch.
|
||||
|
||||
### Key Services
|
||||
- **`MCPBridge`** (`mcp_bridge.go`) — Central GUI service. Registers 29 webview tools + 4 brain tools via HTTP MCP protocol. Routes: `/health`, `/mcp`, `/mcp/tools`, `/mcp/call`, `/ws`.
|
||||
- **`WebviewService`** (`webview_svc.go`) — Wails v3 window management and DOM interaction. Many tools are stubbed pending CDP integration.
|
||||
- **`BrainService`** (`brain_mcp.go`) — Wraps `lifecycle.Client` for OpenBrain vector store (remember/recall/forget). Shared by both modes.
|
||||
- **`ClaudeBridge`** (`claude_bridge.go`) — WebSocket relay to upstream MCP at `ws://localhost:9876/ws`. Currently disabled.
|
||||
|
||||
### Frontend
|
||||
Angular 20+ app embedded via `//go:embed`. Two routes: `/tray` (system tray panel, 380x480 frameless) and `/ide` (full IDE layout). Wails bindings auto-generated in `frontend/bindings/`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `CORE_REPOS` | `host-uk/core,host-uk/core-php,host-uk/core-tenant,host-uk/core-admin` | Repos to poll in headless mode |
|
||||
| `CORE_API_URL` | `http://localhost:8000` | OpenBrain API endpoint |
|
||||
| `CORE_API_TOKEN` | (required) | OpenBrain auth token |
|
||||
|
||||
## Workspace Dependencies
|
||||
|
||||
This module uses a Go workspace (`~/Code/go.work`) with `replace` directives for sibling modules. Ensure these exist alongside `ide/`:
|
||||
- `../go` → `forge.lthn.ai/core/go`
|
||||
- `../go-process` → `forge.lthn.ai/core/go-process`
|
||||
- `../gui` → `forge.lthn.ai/core/gui`
|
||||
|
||||
## Conventions
|
||||
|
||||
- **UK English** in documentation and user-facing strings (colour, organisation, centre).
|
||||
- **Conventional commits**: `type(scope): description` with co-author line `Co-Authored-By: Virgil <virgil@lethean.io>`.
|
||||
- **Licence**: EUPL-1.2.
|
||||
- All Go code is in `package main` (single-package application).
|
||||
- New Wails services: create struct in a Go file, register in `main.go`'s `Services` slice, then `wails3 dev` to regenerate TS bindings.
|
||||
- New MCP tools: add metadata to `handleMCPTools` and a case to `executeWebviewTool` in `mcp_bridge.go`.
|
||||
136
brain_mcp.go
Normal file
136
brain_mcp.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"forge.lthn.ai/core/agent/pkg/lifecycle"
|
||||
)
|
||||
|
||||
// BrainService wraps the lifecycle.Client for OpenBrain vector knowledge store access.
|
||||
// Used by both headless and GUI MCP servers.
|
||||
type BrainService struct {
|
||||
client *lifecycle.Client
|
||||
}
|
||||
|
||||
// NewBrainService creates a BrainService from environment variables.
|
||||
// CORE_API_URL defaults to http://localhost:8000
|
||||
// CORE_API_TOKEN must be set for authentication.
|
||||
func NewBrainService() *BrainService {
|
||||
baseURL := os.Getenv("CORE_API_URL")
|
||||
if baseURL == "" {
|
||||
baseURL = "http://localhost:8000"
|
||||
}
|
||||
token := os.Getenv("CORE_API_TOKEN")
|
||||
return &BrainService{
|
||||
client: lifecycle.NewClient(baseURL, token),
|
||||
}
|
||||
}
|
||||
|
||||
// Remember stores a memory in OpenBrain.
|
||||
func (b *BrainService) Remember(ctx context.Context, content, memType, project, agentID string, tags []string) (*lifecycle.RememberResponse, error) {
|
||||
return b.client.Remember(ctx, lifecycle.RememberRequest{
|
||||
Content: content,
|
||||
Type: memType,
|
||||
Project: project,
|
||||
AgentID: agentID,
|
||||
Tags: tags,
|
||||
})
|
||||
}
|
||||
|
||||
// Recall performs semantic search in OpenBrain.
|
||||
func (b *BrainService) Recall(ctx context.Context, query string, topK int, project, memType, agentID string) (*lifecycle.RecallResponse, error) {
|
||||
return b.client.Recall(ctx, lifecycle.RecallRequest{
|
||||
Query: query,
|
||||
TopK: topK,
|
||||
Project: project,
|
||||
Type: memType,
|
||||
AgentID: agentID,
|
||||
})
|
||||
}
|
||||
|
||||
// Forget removes a memory by ID.
|
||||
func (b *BrainService) Forget(ctx context.Context, id string) error {
|
||||
return b.client.Forget(ctx, id)
|
||||
}
|
||||
|
||||
// EnsureCollection ensures the Qdrant collection exists.
|
||||
func (b *BrainService) EnsureCollection(ctx context.Context) error {
|
||||
return b.client.EnsureCollection(ctx)
|
||||
}
|
||||
|
||||
// executeBrainTool handles brain MCP tool calls. Shared by headless and GUI servers.
|
||||
func executeBrainTool(brain *BrainService, tool string, params map[string]any) map[string]any {
|
||||
if brain == nil {
|
||||
return map[string]any{"error": "brain service not configured"}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch tool {
|
||||
case "brain_remember":
|
||||
content := getStringParam(params, "content")
|
||||
memType := getStringParam(params, "type")
|
||||
if memType == "" {
|
||||
memType = "fact"
|
||||
}
|
||||
project := getStringParam(params, "project")
|
||||
agentID := getStringParam(params, "agent_id")
|
||||
var tags []string
|
||||
if rawTags, ok := params["tags"].([]any); ok {
|
||||
for _, t := range rawTags {
|
||||
if s, ok := t.(string); ok {
|
||||
tags = append(tags, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp, err := brain.Remember(ctx, content, memType, project, agentID, tags)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"id": resp.ID, "type": resp.Type, "project": resp.Project, "created_at": resp.CreatedAt}
|
||||
|
||||
case "brain_recall":
|
||||
query := getStringParam(params, "query")
|
||||
topK := getIntParam(params, "top_k")
|
||||
if topK == 0 {
|
||||
topK = 5
|
||||
}
|
||||
project := getStringParam(params, "project")
|
||||
memType := getStringParam(params, "type")
|
||||
agentID := getStringParam(params, "agent_id")
|
||||
resp, err := brain.Recall(ctx, query, topK, project, memType, agentID)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"memories": resp.Memories, "scores": resp.Scores}
|
||||
|
||||
case "brain_forget":
|
||||
id := getStringParam(params, "id")
|
||||
err := brain.Forget(ctx, id)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
case "brain_ensure_collection":
|
||||
err := brain.EnsureCollection(ctx)
|
||||
if err != nil {
|
||||
return map[string]any{"error": err.Error()}
|
||||
}
|
||||
return map[string]any{"success": true}
|
||||
|
||||
default:
|
||||
return map[string]any{"error": "unknown brain tool", "tool": tool}
|
||||
}
|
||||
}
|
||||
|
||||
// brainToolsList returns the tool definitions for brain MCP tools.
|
||||
func brainToolsList() []map[string]string {
|
||||
return []map[string]string{
|
||||
{"name": "brain_remember", "description": "Store a memory in OpenBrain (content, type, project, agent_id, tags)"},
|
||||
{"name": "brain_recall", "description": "Semantic search in OpenBrain (query, top_k, project, type, agent_id)"},
|
||||
{"name": "brain_forget", "description": "Remove a memory by ID"},
|
||||
{"name": "brain_ensure_collection", "description": "Ensure the Qdrant vector collection exists"},
|
||||
}
|
||||
}
|
||||
28
go.mod
28
go.mod
|
|
@ -3,25 +3,36 @@ module forge.lthn.ai/core/ide
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
forge.lthn.ai/core/go v0.0.0
|
||||
forge.lthn.ai/core/go-process v0.0.0
|
||||
forge.lthn.ai/core/gui v0.0.0
|
||||
forge.lthn.ai/core/agent v0.1.3
|
||||
forge.lthn.ai/core/go-config v0.1.2
|
||||
forge.lthn.ai/core/go-process v0.1.2
|
||||
forge.lthn.ai/core/go-scm v0.1.7
|
||||
forge.lthn.ai/core/go-ws v0.1.3
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.74
|
||||
)
|
||||
|
||||
require (
|
||||
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
forge.lthn.ai/core/go v0.2.2 // indirect
|
||||
forge.lthn.ai/core/go-io v0.0.5 // indirect
|
||||
forge.lthn.ai/core/go-log v0.0.1 // indirect
|
||||
github.com/42wim/httpsig v1.2.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.16.4 // indirect
|
||||
|
|
@ -30,6 +41,7 @@ require (
|
|||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
|
|
@ -42,6 +54,7 @@ require (
|
|||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/redis/go-redis/v9 v9.18.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
|
|
@ -54,6 +67,7 @@ require (
|
|||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.23 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
|
|
@ -62,9 +76,3 @@ require (
|
|||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
forge.lthn.ai/core/go => ../go
|
||||
forge.lthn.ai/core/go-process => ../go-process
|
||||
forge.lthn.ai/core/gui => ../gui
|
||||
)
|
||||
|
|
|
|||
57
go.sum
57
go.sum
|
|
@ -1,5 +1,25 @@
|
|||
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI=
|
||||
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
forge.lthn.ai/core/agent v0.1.3 h1:Swng0OHXPU+gOPyyjIbWxD3u/mFovQpp/UmeXCGQhmc=
|
||||
forge.lthn.ai/core/agent v0.1.3/go.mod h1:FVJdDnpxqQnyJn1sAYFG76UZISw1uM8sOnti9zlpAb4=
|
||||
forge.lthn.ai/core/go v0.2.2 h1:JCWaFfiG+agb0f7b5DO1g+h40x6nb4UydxJ7D+oZk5k=
|
||||
forge.lthn.ai/core/go v0.2.2/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc=
|
||||
forge.lthn.ai/core/go-config v0.1.2 h1:W2Rf7PhVeugTjHZ9XbEr1yMCYY6r1r5fyUmQcM2C9xY=
|
||||
forge.lthn.ai/core/go-config v0.1.2/go.mod h1:6jZZ0XQaSSiWiZjiNfibMEfSPehsBGwfToh//QGhN7E=
|
||||
forge.lthn.ai/core/go-io v0.0.5 h1:oSyngKTkB1gR5fEWYKXftTg9FxwnpddSiCq2dlwfImE=
|
||||
forge.lthn.ai/core/go-io v0.0.5/go.mod h1:ZlU9OQpsvNFNmTJoaHbFIkisZyc0eCq0p8znVWQLRf0=
|
||||
forge.lthn.ai/core/go-log v0.0.1 h1:x/E6EfF9vixzqiLHQOl2KT25HyBcMc9qiBkomqVlpPg=
|
||||
forge.lthn.ai/core/go-log v0.0.1/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
|
||||
forge.lthn.ai/core/go-process v0.1.2 h1:0fdLJq/DPssilN9E5yude/xHNfZRKHghIjo++b5aXgc=
|
||||
forge.lthn.ai/core/go-process v0.1.2/go.mod h1:9oxVALrZaZCqFe8YDdheIS5bRUV1SBz4tVW/MflAtxM=
|
||||
forge.lthn.ai/core/go-scm v0.1.7 h1:UuIqOLal4MUNS4UoWlBBOz1b/13QlcxnMnx9e5fIHWM=
|
||||
forge.lthn.ai/core/go-scm v0.1.7/go.mod h1:3GSEnNWyak3ETGehOW2DEcjWscBlNF5JHu31z2hHTVM=
|
||||
forge.lthn.ai/core/go-ws v0.1.3 h1:TzqFpEcDYcZUFFmrTznfEuVcVdnp2jsRNwAGHeTyXN0=
|
||||
forge.lthn.ai/core/go-ws v0.1.3/go.mod h1:iDbJuR1NT27czjtNIluxnEdLrnfsYQdEBIrsoZnpkCk=
|
||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
|
|
@ -13,6 +33,12 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
|||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
|
|
@ -23,6 +49,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
|
|
@ -35,6 +65,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
|||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
||||
|
|
@ -59,6 +91,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||
|
|
@ -100,6 +134,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
|
|
@ -131,20 +167,31 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
|
||||
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64 h1:xAhLFVfdbg7XdZQ5mMQmBv2BglWu8hMqe50Z+3UJvBs=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64/go.mod h1:zvgNL/mlFcX8aRGu6KOz9AHrMmTBD+4hJRQIONqF/Yw=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.74 h1:wRm1EiDQtxDisXk46NtpiBH90STwfKp36NrTDwOEdxw=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.74/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -159,6 +206,8 @@ golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
|
|
|
|||
26
headless.go
26
headless.go
|
|
@ -11,13 +11,13 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/go-process"
|
||||
"forge.lthn.ai/core/go/pkg/agentci"
|
||||
"forge.lthn.ai/core/go/pkg/config"
|
||||
"forge.lthn.ai/core/go/pkg/forge"
|
||||
"forge.lthn.ai/core/go/pkg/jobrunner"
|
||||
forgejosource "forge.lthn.ai/core/go/pkg/jobrunner/forgejo"
|
||||
"forge.lthn.ai/core/go/pkg/jobrunner/handlers"
|
||||
process "forge.lthn.ai/core/go-process"
|
||||
orchestrator "forge.lthn.ai/core/agent/pkg/orchestrator"
|
||||
config "forge.lthn.ai/core/go-config"
|
||||
"forge.lthn.ai/core/go-scm/forge"
|
||||
"forge.lthn.ai/core/agent/pkg/jobrunner"
|
||||
forgejosource "forge.lthn.ai/core/agent/pkg/jobrunner/forgejo"
|
||||
"forge.lthn.ai/core/agent/pkg/jobrunner/handlers"
|
||||
)
|
||||
|
||||
// hasDisplay returns true if a graphical display is available.
|
||||
|
|
@ -69,18 +69,18 @@ func startHeadless() {
|
|||
|
||||
// Agent dispatch — Clotho integration
|
||||
cfg, cfgErr := config.New()
|
||||
var agentTargets map[string]agentci.AgentConfig
|
||||
var clothoCfg agentci.ClothoConfig
|
||||
var agentTargets map[string]orchestrator.AgentConfig
|
||||
var clothoCfg orchestrator.ClothoConfig
|
||||
|
||||
if cfgErr == nil {
|
||||
agentTargets, _ = agentci.LoadActiveAgents(cfg)
|
||||
clothoCfg, _ = agentci.LoadClothoConfig(cfg)
|
||||
agentTargets, _ = orchestrator.LoadActiveAgents(cfg)
|
||||
clothoCfg, _ = orchestrator.LoadClothoConfig(cfg)
|
||||
}
|
||||
if agentTargets == nil {
|
||||
agentTargets = map[string]agentci.AgentConfig{}
|
||||
agentTargets = map[string]orchestrator.AgentConfig{}
|
||||
}
|
||||
|
||||
spinner := agentci.NewSpinner(clothoCfg, agentTargets)
|
||||
spinner := orchestrator.NewSpinner(clothoCfg, agentTargets)
|
||||
log.Printf("Loaded %d agent targets. Strategy: %s", len(agentTargets), clothoCfg.Strategy)
|
||||
|
||||
dispatch := handlers.NewDispatchHandler(forgeClient, forgeURL, forgeToken, spinner)
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/jobrunner"
|
||||
"forge.lthn.ai/core/agent/pkg/jobrunner"
|
||||
)
|
||||
|
||||
// startHeadlessMCP starts a minimal MCP HTTP server for headless mode.
|
||||
// It exposes job handler tools and health endpoints.
|
||||
// It exposes job handler tools, brain tools, and health endpoints.
|
||||
func startHeadlessMCP(poller *jobrunner.Poller) {
|
||||
brain := NewBrainService()
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -40,6 +42,7 @@ func startHeadlessMCP(poller *jobrunner.Poller) {
|
|||
{"name": "job_set_dry_run", "description": "Enable/disable dry-run mode"},
|
||||
{"name": "job_run_once", "description": "Trigger a single poll-dispatch cycle"},
|
||||
}
|
||||
tools = append(tools, brainToolsList()...)
|
||||
json.NewEncoder(w).Encode(map[string]any{"tools": tools})
|
||||
})
|
||||
|
||||
|
|
@ -78,6 +81,12 @@ func startHeadlessMCP(poller *jobrunner.Poller) {
|
|||
"cycle": poller.Cycle(),
|
||||
})
|
||||
default:
|
||||
// Try brain tools
|
||||
if strings.HasPrefix(req.Tool, "brain_") {
|
||||
result := executeBrainTool(brain, req.Tool, req.Params)
|
||||
json.NewEncoder(w).Encode(result)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"error": "unknown tool"})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,18 +7,19 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/gui/pkg/webview"
|
||||
"forge.lthn.ai/core/gui/pkg/ws"
|
||||
ws "forge.lthn.ai/core/go-ws"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// MCPBridge wires together WebView and WebSocket services
|
||||
// MCPBridge wires together WebView, WebSocket, and Brain services
|
||||
// and starts the MCP HTTP server after Wails initializes.
|
||||
type MCPBridge struct {
|
||||
webview *webview.Service
|
||||
webview *WebviewService
|
||||
brain *BrainService
|
||||
wsHub *ws.Hub
|
||||
claudeBridge *ClaudeBridge
|
||||
app *application.App
|
||||
|
|
@ -29,7 +30,7 @@ type MCPBridge struct {
|
|||
|
||||
// NewMCPBridge creates a new MCP bridge with all services wired up.
|
||||
func NewMCPBridge(port int) *MCPBridge {
|
||||
wv := webview.New()
|
||||
wv := NewWebviewService()
|
||||
hub := ws.NewHub()
|
||||
|
||||
// Create Claude bridge to forward messages to MCP core on port 9876
|
||||
|
|
@ -37,6 +38,7 @@ func NewMCPBridge(port int) *MCPBridge {
|
|||
|
||||
return &MCPBridge{
|
||||
webview: wv,
|
||||
brain: NewBrainService(),
|
||||
wsHub: hub,
|
||||
claudeBridge: claudeBridge,
|
||||
port: port,
|
||||
|
|
@ -75,7 +77,7 @@ func (b *MCPBridge) ServiceStartup(ctx context.Context, options application.Serv
|
|||
// injectConsoleCapture injects the console capture script into windows.
|
||||
func (b *MCPBridge) injectConsoleCapture() {
|
||||
// Wait for windows to be created (poll with timeout)
|
||||
var windows []webview.WindowInfo
|
||||
var windows []WindowInfo
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
windows = b.webview.ListWindows()
|
||||
|
|
@ -190,6 +192,7 @@ func (b *MCPBridge) handleMCPTools(w http.ResponseWriter, r *http.Request) {
|
|||
{"name": "webview_pdf", "description": "Export page as PDF (base64 data URI)"},
|
||||
{"name": "webview_print", "description": "Open print dialog for window"},
|
||||
}
|
||||
tools = append(tools, brainToolsList()...)
|
||||
json.NewEncoder(w).Encode(map[string]any{"tools": tools})
|
||||
}
|
||||
|
||||
|
|
@ -500,6 +503,10 @@ func (b *MCPBridge) executeWebviewTool(tool string, params map[string]any) map[s
|
|||
return map[string]any{"success": true}
|
||||
|
||||
default:
|
||||
// Try brain tools
|
||||
if strings.HasPrefix(tool, "brain_") {
|
||||
return executeBrainTool(b.brain, tool, params)
|
||||
}
|
||||
return map[string]any{"error": "unknown tool", "tool": tool}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
417
webview_svc.go
Normal file
417
webview_svc.go
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// WindowInfo describes a Wails webview window.
|
||||
type WindowInfo struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
// ConsoleEntry is a captured browser console message.
|
||||
type ConsoleEntry struct {
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// WebviewService wraps Wails v3 window management and JS execution
|
||||
// for MCP tool access. This replaces the deleted gui/pkg/webview package.
|
||||
type WebviewService struct {
|
||||
app *application.App
|
||||
console []ConsoleEntry
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewWebviewService creates a new service (no app wired yet).
|
||||
func NewWebviewService() *WebviewService {
|
||||
return &WebviewService{}
|
||||
}
|
||||
|
||||
// SetApp wires the Wails application reference.
|
||||
func (s *WebviewService) SetApp(app *application.App) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.app = app
|
||||
}
|
||||
|
||||
// SetupConsoleListener is a no-op stub — console capture requires
|
||||
// JS injection (see InjectConsoleCapture).
|
||||
func (s *WebviewService) SetupConsoleListener() {}
|
||||
|
||||
// ListWindows returns info for all open Wails windows.
|
||||
func (s *WebviewService) ListWindows() []WindowInfo {
|
||||
s.mu.Lock()
|
||||
app := s.app
|
||||
s.mu.Unlock()
|
||||
|
||||
if app == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []WindowInfo
|
||||
for _, w := range app.Window.GetAll() {
|
||||
result = append(result, WindowInfo{Name: w.Name()})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// getWindow looks up a Wails window by name.
|
||||
func (s *WebviewService) getWindow(name string) (application.Window, error) {
|
||||
s.mu.Lock()
|
||||
app := s.app
|
||||
s.mu.Unlock()
|
||||
|
||||
if app == nil {
|
||||
return nil, fmt.Errorf("app not initialised")
|
||||
}
|
||||
if name == "" {
|
||||
name = "tray-panel"
|
||||
}
|
||||
w, ok := app.Window.Get(name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("window %q not found", name)
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// ExecJS runs JavaScript in the named window and returns the result as a string.
|
||||
// Wails v3 ExecJS is fire-and-forget, so we use a callback pattern via events.
|
||||
func (s *WebviewService) ExecJS(windowName, code string) (string, error) {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
w.ExecJS(code)
|
||||
return "", nil // Wails v3 ExecJS doesn't return values
|
||||
}
|
||||
|
||||
// InjectConsoleCapture injects a JS script that captures console messages.
|
||||
func (s *WebviewService) InjectConsoleCapture(windowName string) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Inject console capture script
|
||||
js := `(function(){
|
||||
if(window.__consoleCaptured) return;
|
||||
window.__consoleCaptured = true;
|
||||
window.__consoleLog = [];
|
||||
['log','warn','error','info','debug'].forEach(function(level){
|
||||
var orig = console[level];
|
||||
console[level] = function(){
|
||||
window.__consoleLog.push({level:level, message:Array.from(arguments).join(' '), ts:Date.now()});
|
||||
if(window.__consoleLog.length > 1000) window.__consoleLog.shift();
|
||||
orig.apply(console, arguments);
|
||||
};
|
||||
});
|
||||
})()`
|
||||
w.ExecJS(js)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConsoleMessages returns captured console messages (requires InjectConsoleCapture).
|
||||
func (s *WebviewService) GetConsoleMessages(level string, limit int) []ConsoleEntry {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
var result []ConsoleEntry
|
||||
for _, e := range s.console {
|
||||
if level != "" && e.Level != level {
|
||||
continue
|
||||
}
|
||||
result = append(result, e)
|
||||
if len(result) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ClearConsole clears the captured console buffer.
|
||||
func (s *WebviewService) ClearConsole() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.console = nil
|
||||
}
|
||||
|
||||
// Click clicks a DOM element by CSS selector.
|
||||
func (s *WebviewService) Click(windowName, selector string) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.ExecJS(fmt.Sprintf(`document.querySelector(%s)?.click()`, jsQuote(selector)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type types text into a DOM element by CSS selector.
|
||||
func (s *WebviewService) Type(windowName, selector, text string) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
js := fmt.Sprintf(`(function(){var el=document.querySelector(%s);if(el){el.focus();el.value=%s;el.dispatchEvent(new Event('input',{bubbles:true}));}})()`,
|
||||
jsQuote(selector), jsQuote(text))
|
||||
w.ExecJS(js)
|
||||
return nil
|
||||
}
|
||||
|
||||
// QuerySelector queries DOM elements by CSS selector.
|
||||
func (s *WebviewService) QuerySelector(windowName, selector string) (any, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("QuerySelector requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// Navigate navigates the window to a URL.
|
||||
func (s *WebviewService) Navigate(windowName, url string) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.SetURL(url)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageSource returns the page HTML source.
|
||||
func (s *WebviewService) GetPageSource(windowName string) (string, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("GetPageSource requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// GetURL returns the current page URL.
|
||||
func (s *WebviewService) GetURL(windowName string) (string, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("GetURL requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// GetTitle returns the current page title.
|
||||
func (s *WebviewService) GetTitle(windowName string) (string, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("GetTitle requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// Screenshot captures the page as base64 PNG.
|
||||
func (s *WebviewService) Screenshot(windowName string) (string, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("Screenshot requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// ScreenshotElement captures a specific element as base64 PNG.
|
||||
func (s *WebviewService) ScreenshotElement(windowName, selector string) (string, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("ScreenshotElement requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// Scroll scrolls the window or element.
|
||||
func (s *WebviewService) Scroll(windowName, selector string, x, y int) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if selector != "" {
|
||||
w.ExecJS(fmt.Sprintf(`document.querySelector(%s)?.scrollTo(%d,%d)`, jsQuote(selector), x, y))
|
||||
} else {
|
||||
w.ExecJS(fmt.Sprintf(`window.scrollTo(%d,%d)`, x, y))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hover dispatches a mouseover event on an element.
|
||||
func (s *WebviewService) Hover(windowName, selector string) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.ExecJS(fmt.Sprintf(`document.querySelector(%s)?.dispatchEvent(new MouseEvent('mouseover',{bubbles:true}))`, jsQuote(selector)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Select selects an option in a dropdown.
|
||||
func (s *WebviewService) Select(windowName, selector, value string) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
js := fmt.Sprintf(`(function(){var el=document.querySelector(%s);if(el){el.value=%s;el.dispatchEvent(new Event('change',{bubbles:true}));}})()`,
|
||||
jsQuote(selector), jsQuote(value))
|
||||
w.ExecJS(js)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check checks or unchecks a checkbox/radio.
|
||||
func (s *WebviewService) Check(windowName, selector string, checked bool) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
js := fmt.Sprintf(`(function(){var el=document.querySelector(%s);if(el){el.checked=%t;el.dispatchEvent(new Event('change',{bubbles:true}));}})()`,
|
||||
jsQuote(selector), checked)
|
||||
w.ExecJS(js)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetElementInfo returns info about a DOM element.
|
||||
func (s *WebviewService) GetElementInfo(windowName, selector string) (any, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("GetElementInfo requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// GetComputedStyle returns computed CSS styles.
|
||||
func (s *WebviewService) GetComputedStyle(windowName, selector string, properties []string) (any, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("GetComputedStyle requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// Highlight visually highlights an element.
|
||||
func (s *WebviewService) Highlight(windowName, selector string, duration int) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if duration <= 0 {
|
||||
duration = 2000
|
||||
}
|
||||
js := fmt.Sprintf(`(function(){var el=document.querySelector(%s);if(el){var old=el.style.outline;el.style.outline='3px solid red';setTimeout(function(){el.style.outline=old;},%d);}})()`,
|
||||
jsQuote(selector), duration)
|
||||
w.ExecJS(js)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDOMTree returns DOM tree structure.
|
||||
func (s *WebviewService) GetDOMTree(windowName string, maxDepth int) (any, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("GetDOMTree requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// GetErrors returns captured error messages.
|
||||
func (s *WebviewService) GetErrors(limit int) []ConsoleEntry {
|
||||
return s.GetConsoleMessages("error", limit)
|
||||
}
|
||||
|
||||
// GetPerformance returns performance metrics.
|
||||
func (s *WebviewService) GetPerformance(windowName string) (any, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("GetPerformance requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// GetResources returns loaded resources.
|
||||
func (s *WebviewService) GetResources(windowName string) (any, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("GetResources requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// GetNetworkRequests returns network request log.
|
||||
func (s *WebviewService) GetNetworkRequests(windowName string, limit int) (any, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("GetNetworkRequests requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// ClearNetworkRequests clears the network request log.
|
||||
func (s *WebviewService) ClearNetworkRequests(windowName string) error {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("ClearNetworkRequests requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// InjectNetworkInterceptor injects network monitoring JS.
|
||||
func (s *WebviewService) InjectNetworkInterceptor(windowName string) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
js := `(function(){
|
||||
if(window.__networkCaptured) return;
|
||||
window.__networkCaptured = true;
|
||||
window.__networkLog = [];
|
||||
var origFetch = window.fetch;
|
||||
window.fetch = function(){
|
||||
var start = Date.now();
|
||||
var url = arguments[0];
|
||||
if(typeof url === 'object') url = url.url;
|
||||
return origFetch.apply(this, arguments).then(function(resp){
|
||||
window.__networkLog.push({url:url, status:resp.status, duration:Date.now()-start, ts:start});
|
||||
if(window.__networkLog.length > 500) window.__networkLog.shift();
|
||||
return resp;
|
||||
});
|
||||
};
|
||||
})()`
|
||||
w.ExecJS(js)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportToPDF exports page as PDF via CDP.
|
||||
func (s *WebviewService) ExportToPDF(windowName string, options map[string]any) (string, error) {
|
||||
_, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("ExportToPDF requires CDP — not yet implemented")
|
||||
}
|
||||
|
||||
// PrintToPDF opens print dialog.
|
||||
func (s *WebviewService) PrintToPDF(windowName string) error {
|
||||
w, err := s.getWindow(windowName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.ExecJS(`window.print()`)
|
||||
return nil
|
||||
}
|
||||
|
||||
// jsQuote JSON-encodes a string for safe JS interpolation.
|
||||
func jsQuote(s string) string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue