Skip to content

Commit fa91dd9

Browse files
author
SqlRush
committed
Add native clipboard status command
1 parent c10d6df commit fa91dd9

4 files changed

Lines changed: 104 additions & 2 deletions

File tree

docs/claude-code-go-rewrite-plan.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,8 @@ test/parity/ # golden tests against TS/official behavior
863863

864864
- 本轮补充:新增 `advanced` settings gate 地基,覆盖 bridge/LSP/telemetry/Chrome/voice/computer-use/native integrations 的独立 bool 开关解析、settings merge、headless `/config show advanced` 和 `/config search` 审计;`advanced.telemetry=true` 时会在 session 目录写入安全摘要 JSONL 诊断事件,记录事件类型、session/model、tool/progress keys、token/compact/error 摘要,不写入用户/助手正文或工具结果内容,并已有安全 JSONL 读取、类型/模型过滤、汇总统计、deterministic trace/span id、JSON summary export、可配置 `telemetryExport` 本地 JSONL exporter / HTTP backend POST、endpoint 脱敏 status,以及 headless `/status show telemetry` trace/span/exporter 审计地基;`advanced.lsp=true` 时才向模型暴露只读 `LSPDiagnostics` 工具,用于读取 session-scoped diagnostics snapshot,并支持 file/severity/limit 过滤,底层已能解析 LSP `textDocument/publishDiagnostics` params/notification payload、按文件替换 snapshot、按 LSP 空 diagnostics 语义清空旧文件诊断;同时会写出 session-scoped LSP manager status,按默认或显式 server definitions 解析 workspace/root marker/file extension 命中情况,将已匹配但未启动的 server 标为 `not_started`,并可通过 headless `/status show lsp` 审计 diagnostics 与 manager runtime state;LSP Content-Length framed JSON-RPC reader/writer 与 diagnostics stream processor 已能持续消费 `textDocument/publishDiagnostics` notification、捕获 initialize response capabilities 并更新 session snapshot;LSP server process lifecycle API 已可启动配置命令、消费 stdout framed diagnostics stream,并把 `running`/`exited`/`failed`、PID 和时间戳写回 manager status;LSP client handshake send path 已可向 server stdin 写入 `initialize`、`initialized` 和 `textDocument/didOpen` framed JSON-RPC,支持 root/client/document metadata 和 file URI/language 默认推断;conversation runner 在 `advanced.lsp=true` 且显式或默认 server definitions 命中 workspace 时,会自动启动可执行命令存在的 LSP server、发送 handshake/startup documents、对缺失命令保持可审计 `not_started` reason,并复用 session diagnostics/manager status 生命周期;`advanced.bridge=true` 时会写出 session-scoped bridge manifest 和 direct connect state,列出 bridge-safe slash/local command 元数据,启动 loopback-only direct HTTP/WebSocket endpoint,提供 `/health`、`/manifest`、`/resolve`、`/execute` 和 `/ws` JSON 通道,`/ws` 支持 `hello`/`health`/`manifest`/`resolve`/`execute`/`remote_trigger` action 与 protocol version/capability 握手,执行前强制 bridge-safe 白名单并默认不回传展开后的 prompt 正文,支持可选 bearer/`X-Bridge-Token` token guard,并可通过 headless `/status show bridge` 审计 manifest、HTTP URL、WebSocket URL 和 token-required 状态;`advanced.nativeIntegrations=true` 时会写出 session-scoped native capability manifest、native file index、native clipboard state 和 system/tmux/OSC52 clipboard adapter 检测结果,只记录路径/大小/mtime 等文件元数据、跳过常见 runtime/vendor 目录,clipboard status 不展示文本内容,并已有 ANSI color diff rendering runtime、system/tmux 外部剪贴板命令读写执行 API 和显式 `/native clipboard read|write` 用户路径,可通过 headless `/status show native` 审计 index/clipboard 路径、数量与 adapter 命令计划;任一 `advanced.chrome`/`advanced.voice`/`advanced.computerUse=true` 时会写出 session-scoped integrations manifest 和独立 runtime state file,将 Chrome/voice/computer-use 的 enabled 与 `runtime_state=ready` 分开记录,并记录 browser/native-host、audio-capture、screen-capture/input-control adapter 探测结果;`advanced.chrome=true` 时还会写出 session-scoped Chrome native host manifest 与 wrapper artifact,并已有 `--chrome-native-host` fast path、Chrome native messaging length-prefixed JSON frame 编解码和 ping/status/hello/capabilities/session/runtime 响应,以及显式 `/native chrome status|install` 用户路径,可将 manifest/wrapper 安装到 macOS/Linux Chrome/Chromium/Edge NativeMessagingHosts 目录,并在 Windows 通过 HKCU NativeMessagingHosts 注册表项注册 manifest;`advanced.voice=true` 时会写出 session-scoped voice capture plan artifact,记录选中的 audio-capture adapter、16kHz/mono/pcm_s16le/streaming 参数,并已有受 duration/max-bytes 限制的 voice capture command runner API、可配置 `CLAUDE_VOICE_TRANSCRIBE_COMMAND` 的 STT command runner,以及显式 `/native voice status|show|capture|transcribe` 用户路径;`advanced.computerUse=true` 时会写出 session-scoped computer-use driver plan artifact,记录 screen-capture/input-control adapter、png 截图格式和 screen_pixels 坐标系,并已有 screen capture stdout 捕获、显式 `/native computer status|show|screenshot` 与 `/native computer move|click|type|key` 用户路径,以及 xdotool、macOS osascript、Windows PowerShell 输入执行 API,可通过 headless `/status show integrations` 审计 runtime state 路径与 adapter/manifest/voice/computer-use plan 命令计划;未启用时仍不注册或泄露 gated 工具 schema。实际 Chrome 扩展 UI/事件通道、voice 实时/服务端 STT 体验、computer-use Wayland/权限边界与更深交互接入等平台增强仍未完成。
865865

866+
- 本轮补充:`/native clipboard status|show` 现在走 no-query 本地状态路径,只展示 session clipboard 文件路径、item 数、更新时间和 adapter 可用性/命令,不读取、不写入、不展示剪贴板文本内容;`/native clipboard` usage 同步暴露 status 子命令。
867+
866868
### M12: Parity hardening
867869

868870
产出:

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Anthropic API 和 conversation:
8181
- `/cost status`, `/cost current`, and `/cost usage` now share the no-query totals path with `/cost`, while `/cost breakdown`/`show` continue to return per-message usage details and unknown subcommands remain explicit.
8282
- `/config <section>` now shares the same section-alias normalization as `/config show <section>` for non-mutating direct section invocations such as `/config env`, `/config permissions`, and `/config integrations`, while preserving existing mutating forms such as `/config model <name>` and `/config output-style <name>`.
8383
- `/memory list`, `/memory files`, and `/memory ls` now take the no-query local file-list path used by `/memory show`, while `/memory status` continues to report the session/relevant-memory summary.
84+
- `/native clipboard status|show` now takes a no-query local status path that reports the session clipboard state and adapter availability without reading, writing, or displaying clipboard text.
8485
- `/native voice status|show` and `/native computer status|show` now take no-query local status paths that report the session plan path, adapter availability, selected commands, and voice/computer-use runtime parameters without starting capture or input actions.
8586
- `/mcp <server-name>` now takes the no-query local server-detail path used by `/mcp show <server-name>` when the name matches a configured settings or plugin MCP server, while unknown one-word subcommands still report unsupported.
8687
- `/plugin <plugin-name>` now takes the no-query local plugin-detail path used by `/plugin show <plugin-name>` when the name matches a local plugin manifest, while unknown one-word subcommands still report unsupported.

internal/conversation/run.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,14 +1294,16 @@ func (r Runner) formatNativeChromeCommand(ctx context.Context, raw string) strin
12941294
func (r Runner) formatNativeClipboardCommand(ctx context.Context, raw string) string {
12951295
args := strings.Fields(strings.TrimSpace(raw))
12961296
if len(args) == 0 {
1297-
return "Usage: /native clipboard <read|write <text>>"
1297+
return nativeClipboardUsage()
12981298
}
12991299
clipboardPath := nativepkg.SessionClipboardPath(r.SessionPath, r.SessionID)
13001300
if clipboardPath == "" {
13011301
return "Native clipboard\nClipboard path: (not configured)"
13021302
}
13031303
adapters := nativepkg.DetectClipboardAdapters(nativepkg.ClipboardAdapterOptions{})
13041304
switch strings.ToLower(args[0]) {
1305+
case "status", "show":
1306+
return strings.Join(formatNativeClipboardStatusLines(clipboardPath, adapters), "\n")
13051307
case "write", "set", "copy":
13061308
text := strings.TrimSpace(dropLeadingFields(raw, 1))
13071309
if text == "" {
@@ -1336,8 +1338,53 @@ func (r Runner) formatNativeClipboardCommand(ctx context.Context, raw string) st
13361338
lines = append(lines, "Text: "+text)
13371339
return strings.Join(lines, "\n")
13381340
default:
1339-
return "Usage: /native clipboard <read|write <text>>"
1341+
return nativeClipboardUsage()
1342+
}
1343+
}
1344+
1345+
func nativeClipboardUsage() string {
1346+
return "Usage: /native clipboard <status|read|write <text>>"
1347+
}
1348+
1349+
func formatNativeClipboardStatusLines(clipboardPath string, adapters []nativepkg.ClipboardAdapter) []string {
1350+
lines := []string{
1351+
"Native clipboard status",
1352+
"Clipboard path: " + clipboardPath,
13401353
}
1354+
clipboard, err := nativepkg.LoadClipboard(clipboardPath)
1355+
if err != nil {
1356+
lines = append(lines, "Clipboard state error: "+err.Error())
1357+
} else {
1358+
lines = append(lines, fmt.Sprintf("Session clipboard items: %d", len(clipboard.Items)))
1359+
if clipboard.UpdatedAt != "" {
1360+
lines = append(lines, "Clipboard updated: "+clipboard.UpdatedAt)
1361+
}
1362+
}
1363+
lines = append(lines, fmt.Sprintf("Clipboard adapters: %d", nativepkg.CountAvailableClipboardAdapters(adapters)))
1364+
if len(adapters) > 0 {
1365+
lines = append(lines, "Adapter states:")
1366+
for _, adapter := range adapters {
1367+
state := "unavailable"
1368+
if adapter.Available {
1369+
state = "available"
1370+
}
1371+
line := "- " + adapter.Name + ": " + state
1372+
if adapter.Kind != "" {
1373+
line += " (" + adapter.Kind + ")"
1374+
}
1375+
if len(adapter.WriteCommand) > 0 {
1376+
line += " write=" + strings.Join(adapter.WriteCommand, " ")
1377+
}
1378+
if len(adapter.ReadCommand) > 0 {
1379+
line += " read=" + strings.Join(adapter.ReadCommand, " ")
1380+
}
1381+
if adapter.Detail != "" {
1382+
line += " - " + adapter.Detail
1383+
}
1384+
lines = append(lines, line)
1385+
}
1386+
}
1387+
return lines
13411388
}
13421389

13431390
func (r Runner) formatNativeVoiceCommand(ctx context.Context, raw string) string {

internal/conversation/run_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2787,6 +2787,58 @@ func TestRunnerExecutesNativeClipboardReadCommandWithoutQuery(t *testing.T) {
27872787
}
27882788
}
27892789

2790+
func TestRunnerNativeClipboardStatusCommandWithoutQuery(t *testing.T) {
2791+
setupFakeClipboardCommandPath(t)
2792+
client := &fakeClient{}
2793+
dir := t.TempDir()
2794+
sessionPath := filepath.Join(dir, "session.jsonl")
2795+
sessionID := contracts.ID("sess_native_clipboard_status")
2796+
clipboardPath := nativepkg.SessionClipboardPath(sessionPath, sessionID)
2797+
if _, err := nativepkg.WriteClipboardText(clipboardPath, sessionID, "clipboard", "secret status text"); err != nil {
2798+
t.Fatal(err)
2799+
}
2800+
runnerCalled := false
2801+
runner := Runner{
2802+
Client: client,
2803+
Model: "sonnet",
2804+
SessionID: sessionID,
2805+
SessionPath: sessionPath,
2806+
WorkingDirectory: dir,
2807+
NativeClipboardRunner: func(ctx context.Context, command []string, stdin string) (string, error) {
2808+
runnerCalled = true
2809+
return "", nil
2810+
},
2811+
}
2812+
for _, prompt := range []string{"/native clipboard status", "/native clipboard show"} {
2813+
result, err := runner.RunTurn(context.Background(), nil, messages.UserText(prompt))
2814+
if err != nil {
2815+
t.Fatal(err)
2816+
}
2817+
if len(client.requests) != 0 {
2818+
t.Fatalf("%s queried model, requests = %d", prompt, len(client.requests))
2819+
}
2820+
if runnerCalled {
2821+
t.Fatalf("%s invoked external clipboard runner", prompt)
2822+
}
2823+
text := result.Messages[len(result.Messages)-1].Content[0].Text
2824+
for _, want := range []string{
2825+
"Native clipboard status",
2826+
"Clipboard path: " + clipboardPath,
2827+
"Session clipboard items: 1",
2828+
"Clipboard updated:",
2829+
"Clipboard adapters:",
2830+
"- osc52: available (terminal)",
2831+
} {
2832+
if !strings.Contains(text, want) {
2833+
t.Fatalf("%s status missing %q: %q", prompt, want, text)
2834+
}
2835+
}
2836+
if strings.Contains(text, "secret status text") {
2837+
t.Fatalf("%s leaked clipboard text: %q", prompt, text)
2838+
}
2839+
}
2840+
}
2841+
27902842
func TestRunnerExecutesNativeChromeInstallCommandWithoutQuery(t *testing.T) {
27912843
client := &fakeClient{}
27922844
dir := t.TempDir()

0 commit comments

Comments
 (0)