Skip to content

Commit c5428e3

Browse files
author
SqlRush
committed
Accept provider interaction scripts
1 parent c753db8 commit c5428e3

5 files changed

Lines changed: 169 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
606606

607607
本轮补充:interaction script loader 现在会扁平化 `cases`/`tests`/`testCases`/`scenarios`/`fixtures` 等 suite array,每个 case 内的 `steps`/`timeline`/`scriptSteps` 会按顺序展开,顶层数组也可直接混入 case object。
608608

609+
本轮补充:interaction script loader 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` response wrapper,可从 `message.content`、content-block array 和 `content.parts[].text` 里递归恢复 script JSON。
610+
609611
本轮补充:interaction script 的 key/keySequence action payload 现在递归解包 JSON:API/GraphQL-style wrapper,wrapped single key 和 key sequence array 可直接驱动按键输入与组合键事件。
610612

611613
本轮补充:interaction script 的 direct key alias 字段现在也接受 wrapped object,`key``keyPress``keyPresses` 等字段可从 `resource.attributes``edge.node.attrs` 中恢复单键和 key sequence,避免官方 fixture 直接字段形态在 string/list decode 阶段失败。
@@ -702,6 +704,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
702704

703705
本轮补充:interaction script loader 现在接受 GraphQL connection 风格的 `edges` step list 和 JSON:API/HAL collection 风格的 `included``collection`/`list`/`children`/`values` step list,数组元素可用 `edges[].node``edge.node``resource.attributes``resource.properties` wrapper,外层也可递归解包 `viewer`/`node`/`*Connection` wrapper 来加载录制脚本。
704706

707+
本轮补充:interaction script loader 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` response wrapper,可从 `message.content`、content-block array 和 `content.parts[].text` 里恢复 script JSON,减少模型/SDK 录制脚本响应的手工拆包。
708+
705709
本轮补充:TUI Vim prompt editing 增加基础 visual/visual-line 模式,支持 `v`/`V` 进入选择、motion 扩展 selection、`y`/`d`/`c` 作用于选择范围、Esc 回到 normal,并让 interaction script 可用 `visual`/`visualLine` 断言当前 mode。
706710

707711
本轮补充:prompt history 写入现在保留 image pasted-content 的 media type、filename、dimensions 和 image-cache source path 元数据,同时继续不把 inline base64 image bytes 或 text-paste hash 写进图片历史记录。

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ test/parity/ # golden tests against TS/official behavior
327327
- 本轮补充:interaction script 的 `keys` 字段支持 printable text chunk 和空格分隔 named-key sequence,例如 `ctrl-x ctrl-k`,减少官方脚本把连续输入拆成数组的改写成本。
328328
- 本轮补充:interaction script key input 接受 press-style aliases,包括 `press``keyPress``keypress``shortcutKey``presses``keyPresses``shortcuts`
329329
- 本轮补充:interaction script loader 现在会扁平化 `cases`/`tests`/`testCases`/`scenarios`/`fixtures` 等 suite array,每个 case 内的 `steps`/`timeline`/`scriptSteps` 会按顺序展开,顶层数组也可直接混入 case object。
330+
- 本轮补充:interaction script loader 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` response wrapper,可从 `message.content`、content-block array 和 `content.parts[].text` 里递归恢复 script JSON。
330331
- 本轮补充:interaction script key/keySequence action payload 现在递归解包 JSON:API/GraphQL-style wrapper,`payload.resource.attributes.key``edge.node.attrs.sequence` 可直接驱动按键与组合键序列。
331332
- 本轮补充:interaction script 的直接字符串 alias 字段现在也接受 wrapped object;`text``pasteText``setStatus``snapshotName` 等字段可从 `resource.attributes``edge.node.attrs` 中恢复正文、paste、status 和 snapshot 名称,避免 direct field fixture 在 scalar decode 阶段失败。
332333
- 本轮补充:interaction script action/type/kind/name/operation 动作判别字段接受 compact/camel fixture aliases,包括 `typeText``inputText``insertText``keyPress``pressKey``keySequence``pasteText``pastedText``clipboardText``setStatus``statusLine``terminalSize``screenSize`
@@ -438,6 +439,7 @@ test/parity/ # golden tests against TS/official behavior
438439
- 本轮补充:interaction script 的单个 step item 现在也接受 JSON:API/resource-style `resource`/`node`/`attributes`/`properties` wrapper,数组元素和 JSONL 行可直接使用 API fixture 的 step resource 形态。
439440
- 本轮补充:interaction script loader 现在把 `data`/`payload`/`body`/`result`/`response``resources``nodes` 中的数组也视为 step list,可直接加载 API/GraphQL collection envelope,同时保留单步 `data` 载荷兼容。
440441
- 本轮补充:interaction script loader 现在接受 GraphQL connection 风格的 `edges` step list 和 JSON:API/HAL collection 风格的 `included``collection`/`list`/`children`/`values` step list,数组元素可用 `edges[].node``edge.node``resource.attributes``resource.properties` wrapper,外层也可递归解包 `viewer`/`node`/`*Connection` wrapper 来加载录制脚本。
442+
- 本轮补充:interaction script loader 现在也接受 provider-style `choices`/`outputs`/`candidates`/`generations` response wrapper,可从 `message.content`、content-block array 和 `content.parts[].text` 里恢复 script JSON,减少模型/SDK 录制脚本响应的手工拆包。
441443
- 本轮补充:ANSI snapshot corpus 比对支持 `.ansi` only baseline fallback,strict stale-baseline 检查同时覆盖 `.txt``.ansi`
442444
- 本轮补充:interaction script JSONL loader 单行上限提升到 50MiB,和 transcript/session 大记录读取容忍度对齐,覆盖大型 paste、image metadata 或 snapshot fixture 脚本行。
443445
- 本轮补充:terminal lifecycle 增加可选 extended-key mode,按官方 `CSI >1u`/`CSI >4;2m` 启用 kitty keyboard protocol 和 modifyOtherKeys,退出时重置 modifyOtherKeys 并 pop kitty stack,reassert 时先 pop 再 push 以避免长期会话 stack 泄漏。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ M7 progress now includes:
308308
- `internal/tui`: scripted interaction `keys` entries now accept printable text chunks and whitespace-separated named key sequences such as `ctrl-x ctrl-k`.
309309
- `internal/tui`: scripted interaction key input now accepts press-style aliases such as `press`, `keyPress`, `keypress`, `shortcutKey`, `presses`, `keyPresses`, and `shortcuts`.
310310
- `internal/tui`: interaction script loading now flattens suite/case arrays such as `cases`, `tests`, `testCases`, `scenarios`, and `fixtures`, expanding nested `steps`, `timeline`, or `scriptSteps` arrays in order while still allowing top-level arrays to mix direct steps and case objects.
311+
- `internal/tui`: interaction script loading now also unwraps provider-style `choices`, `outputs`, `candidates`, and `generations` responses, recovering script JSON from nested `message.content`, content-block arrays, and `content.parts[].text`.
311312
- `internal/tui`: scripted key and key-sequence action payloads now recursively unwrap JSON:API/GraphQL-style wrappers, preserving wrapped single keys and key sequence arrays from API-shaped interaction fixtures.
312313
- `internal/tui`: direct scripted key alias fields such as `key`, `keyPress`, and `keyPresses` now accept JSON:API/GraphQL-style wrapper objects without failing strong string/list decoding.
313314
- `internal/tui`: direct scripted string alias fields such as `text`, `pasteText`, `setStatus`, and `snapshotName` now accept JSON:API/GraphQL-style wrapper objects without failing strong scalar decoding.

internal/tui/script_loader.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"os"
9+
"strings"
910
)
1011

1112
const maxInteractionScriptLineBytes = 50 * 1024 * 1024
@@ -37,6 +38,13 @@ func ParseInteractionScript(data []byte) ([]ScriptStep, error) {
3738
if ok || err != nil {
3839
return steps, err
3940
}
41+
if text, ok := interactionScriptProviderResponseText(data); ok {
42+
steps, err := ParseInteractionScript([]byte(text))
43+
if err != nil {
44+
return nil, fmt.Errorf("parse interaction script provider response: %w", err)
45+
}
46+
return steps, nil
47+
}
4048
}
4149

4250
scanner := bufio.NewScanner(bytes.NewReader(data))
@@ -319,6 +327,96 @@ func interactionScriptObjectHasContainer(data []byte) bool {
319327
return false
320328
}
321329

330+
func interactionScriptProviderResponseText(data []byte) (string, bool) {
331+
var object map[string]json.RawMessage
332+
if err := json.Unmarshal(data, &object); err != nil {
333+
return "", false
334+
}
335+
if scriptStepJSONHasDirectFields(object) {
336+
return "", false
337+
}
338+
for _, name := range []string{
339+
"choice",
340+
"choices",
341+
"output",
342+
"outputs",
343+
"candidate",
344+
"candidates",
345+
"generation",
346+
"generations",
347+
"completion",
348+
"completions",
349+
"response",
350+
"responses",
351+
"result",
352+
"results",
353+
} {
354+
value, ok := object[name]
355+
if !ok {
356+
continue
357+
}
358+
if text, ok := interactionScriptProviderTextFromRaw(value, 0, false); ok {
359+
return text, true
360+
}
361+
}
362+
return "", false
363+
}
364+
365+
func interactionScriptProviderTextFromRaw(raw json.RawMessage, depth int, allowScalar bool) (string, bool) {
366+
raw = bytes.TrimSpace(raw)
367+
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) || depth > 8 {
368+
return "", false
369+
}
370+
var text string
371+
if err := json.Unmarshal(raw, &text); err == nil {
372+
text = strings.TrimSpace(text)
373+
return text, allowScalar && text != ""
374+
}
375+
if raw[0] == '[' {
376+
var items []json.RawMessage
377+
if err := json.Unmarshal(raw, &items); err != nil {
378+
return "", false
379+
}
380+
parts := make([]string, 0, len(items))
381+
for _, item := range items {
382+
part, ok := interactionScriptProviderTextFromRaw(item, depth+1, false)
383+
if ok {
384+
parts = append(parts, part)
385+
}
386+
}
387+
if len(parts) == 0 {
388+
return "", false
389+
}
390+
return strings.Join(parts, "\n"), true
391+
}
392+
if raw[0] != '{' {
393+
return "", false
394+
}
395+
fields := map[string]json.RawMessage{}
396+
if err := json.Unmarshal(raw, &fields); err != nil {
397+
return "", false
398+
}
399+
for _, name := range []string{"text", "content", "value", "output"} {
400+
value, ok := fields[name]
401+
if !ok {
402+
continue
403+
}
404+
if text, ok := interactionScriptProviderTextFromRaw(value, depth+1, true); ok {
405+
return text, true
406+
}
407+
}
408+
for _, name := range []string{"message", "delta", "part", "parts", "candidate", "choice", "generation", "result", "response"} {
409+
value, ok := fields[name]
410+
if !ok {
411+
continue
412+
}
413+
if text, ok := interactionScriptProviderTextFromRaw(value, depth+1, false); ok {
414+
return text, true
415+
}
416+
}
417+
return "", false
418+
}
419+
322420
func parseInteractionScriptOptionalStepsValue(value json.RawMessage) ([]ScriptStep, bool, error) {
323421
value = bytes.TrimSpace(value)
324422
if len(value) == 0 || bytes.Equal(value, []byte("null")) {

internal/tui/tui_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8614,6 +8614,70 @@ func TestParseInteractionScriptAcceptsWrapperObjects(t *testing.T) {
86148614
}
86158615
}
86168616

8617+
func TestParseInteractionScriptAcceptsProviderResponseWrappers(t *testing.T) {
8618+
scriptArray := []map[string]any{
8619+
{"action": "type", "value": "provider"},
8620+
{"type": "press", "value": "enter", "expectEvent": map[string]any{"type": "prompt_submitted", "value": "provider"}},
8621+
}
8622+
scriptArrayData, err := json.Marshal(scriptArray)
8623+
if err != nil {
8624+
t.Fatal(err)
8625+
}
8626+
scriptObjectData, err := json.Marshal(map[string]any{"steps": scriptArray})
8627+
if err != nil {
8628+
t.Fatal(err)
8629+
}
8630+
tests := map[string]any{
8631+
"choices_message_content": map[string]any{
8632+
"choices": []any{map[string]any{
8633+
"message": map[string]any{"content": string(scriptArrayData)},
8634+
}},
8635+
},
8636+
"candidate_parts_text": map[string]any{
8637+
"candidates": []any{map[string]any{
8638+
"content": map[string]any{
8639+
"parts": []any{map[string]any{"text": string(scriptObjectData)}},
8640+
},
8641+
}},
8642+
},
8643+
}
8644+
for name, wrapper := range tests {
8645+
t.Run(name, func(t *testing.T) {
8646+
data, err := json.Marshal(wrapper)
8647+
if err != nil {
8648+
t.Fatal(err)
8649+
}
8650+
steps, err := ParseInteractionScript(data)
8651+
if err != nil {
8652+
t.Fatal(err)
8653+
}
8654+
if len(steps) != 2 {
8655+
t.Fatalf("steps = %#v", steps)
8656+
}
8657+
screen := NewREPLScreen(30, 6, nil)
8658+
result, err := RunInteractionScriptChecked(&screen, steps)
8659+
if err != nil {
8660+
t.Fatal(err)
8661+
}
8662+
if len(result.Events) != 1 || result.Events[0].Type != ScreenEventPromptSubmitted || result.Events[0].Value != "provider" {
8663+
t.Fatalf("events = %#v", result.Events)
8664+
}
8665+
})
8666+
}
8667+
8668+
steps, err := ParseInteractionScript([]byte(`{"action":"type","value":"direct","result":{"text":"[{\"action\":\"type\",\"value\":\"wrong\"}]"}}`))
8669+
if err != nil {
8670+
t.Fatal(err)
8671+
}
8672+
screen := NewREPLScreen(30, 6, nil)
8673+
if _, err := RunInteractionScriptChecked(&screen, steps); err != nil {
8674+
t.Fatal(err)
8675+
}
8676+
if screen.Prompt.Text != "direct" {
8677+
t.Fatalf("direct step was treated as provider response: %#v", steps)
8678+
}
8679+
}
8680+
86178681
func TestParseInteractionScriptAcceptsSuiteCaseArrays(t *testing.T) {
86188682
for name, script := range map[string]string{
86198683
"cases": `{"cases":[

0 commit comments

Comments
 (0)