feat: add brain MCP tools and webview service
Some checks failed
Security Scan / security (push) Successful in 8s
Test / test (push) Failing after 1m12s

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:
Snider 2026-03-13 09:30:05 +00:00
parent e2c6a79487
commit 5a2d508254
8 changed files with 735 additions and 35 deletions

74
CLAUDE.md Normal file
View 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
View 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
View file

@ -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
View file

@ -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=

View file

@ -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)

View file

@ -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"})
}
})

View file

@ -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
View 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)
}