one-code-extension 0.1.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +222 -177
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +128 -0
- package/docs/guide/configuration.md +173 -0
- package/docs/guide/differences-from-claude-code.md +152 -0
- package/docs/guide/doctor.md +88 -0
- package/docs/guide/hooks.md +111 -0
- package/docs/guide/installation.md +126 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
- package/docs/guide/providers-and-models.md +196 -0
- package/docs/guide/reference.md +226 -0
- package/docs/guide/sessions-and-context.md +192 -0
- package/docs/guide/skills-plugins-and-mcp.md +178 -0
- package/docs/guide/subagents-and-workflows.md +195 -0
- package/docs/guide/tasks-and-background-work.md +113 -0
- package/docs/guide/terminal-interface.md +157 -0
- package/docs/guide/tools.md +177 -0
- package/docs/guide/troubleshooting.md +177 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The multi-question widget behind ask_user_question (pure): state machine and
|
|
3
|
+
* rendering for Claude Code's tabbed question dialog — one tab per question
|
|
4
|
+
* plus Submit, a side-by-side preview pane on single-select questions whose
|
|
5
|
+
* options carry `preview`, per-question notes ("press n to add notes"),
|
|
6
|
+
* multi-select checkboxes, an inline free-text row, and "Chat about this",
|
|
7
|
+
* which hands the whole batch back to the conversation. The thin ctx.ui.custom
|
|
8
|
+
* component in index.ts owns nothing but this state and repaint calls — same
|
|
9
|
+
* split as plan-mode's viewer.
|
|
10
|
+
*
|
|
11
|
+
* Every rendered line must stay within the width given: pi-tui crashes the
|
|
12
|
+
* whole app on an overwide line, so renderWidget runs each line through the
|
|
13
|
+
* ANSI-aware truncateLine as a final guard, cutting plain text before
|
|
14
|
+
* painting wherever it can along the way.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { cutPlainText, padPlainText, truncateLine, visibleWidth, wrapPlainText } from "../lib/tui-render.ts";
|
|
18
|
+
import type { Answer, Question } from "./questions.ts";
|
|
19
|
+
|
|
20
|
+
export type Paint = (color: string, text: string) => string;
|
|
21
|
+
export type Bold = (text: string) => string;
|
|
22
|
+
|
|
23
|
+
/** The theme surface the widget paints with (safeTheme* wrappers from index.ts). */
|
|
24
|
+
export interface WidgetStyle {
|
|
25
|
+
paint: Paint;
|
|
26
|
+
bold: Bold;
|
|
27
|
+
/** Reverse video — the active tab's background chip. */
|
|
28
|
+
inverse: (text: string) => string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TAB_UNANSWERED = "⊡";
|
|
32
|
+
const TAB_ANSWERED = "⊠";
|
|
33
|
+
const OTHER_PLACEHOLDER = "Type something.";
|
|
34
|
+
const CHAT_LABEL = "Chat about this";
|
|
35
|
+
const NOTES_PLACEHOLDER = "press n to add notes";
|
|
36
|
+
const MAX_PREVIEW_ROWS = 12;
|
|
37
|
+
|
|
38
|
+
// ── state ────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export interface QuestionState {
|
|
41
|
+
cursor: number;
|
|
42
|
+
/** Selected option indices (at most one unless multiSelect). */
|
|
43
|
+
selected: number[];
|
|
44
|
+
otherText: string;
|
|
45
|
+
/** Typed free-text is part of the answer. Only ever set with a non-empty otherText. */
|
|
46
|
+
otherChosen: boolean;
|
|
47
|
+
notes: string;
|
|
48
|
+
/** Multi-select "Next" pressed — counts as answered even with nothing ticked. */
|
|
49
|
+
committed: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WidgetState {
|
|
53
|
+
questions: Question[];
|
|
54
|
+
/** 0..N-1 = question tabs, N = the Submit tab. */
|
|
55
|
+
tab: number;
|
|
56
|
+
/** Present while typing into the free-text row or the notes line. */
|
|
57
|
+
editing?: { field: "other" | "notes"; draft: string };
|
|
58
|
+
qs: QuestionState[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type WidgetResult =
|
|
62
|
+
| { kind: "submit"; answers: Answer[] }
|
|
63
|
+
| { kind: "chat" }
|
|
64
|
+
/** Esc; `answers` holds what the user had picked on other tabs, never submitted. */
|
|
65
|
+
| { kind: "cancel"; answers: Answer[] };
|
|
66
|
+
|
|
67
|
+
export function createWidgetState(questions: Question[]): WidgetState {
|
|
68
|
+
return {
|
|
69
|
+
questions,
|
|
70
|
+
tab: 0,
|
|
71
|
+
qs: questions.map(() => ({
|
|
72
|
+
cursor: 0,
|
|
73
|
+
selected: [],
|
|
74
|
+
otherText: "",
|
|
75
|
+
otherChosen: false,
|
|
76
|
+
notes: "",
|
|
77
|
+
committed: false,
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── row model ────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export type Row = { kind: "option"; index: number } | { kind: "other" } | { kind: "next" } | { kind: "chat" };
|
|
85
|
+
|
|
86
|
+
/** Preview pane applies to single-select questions with at least one preview. */
|
|
87
|
+
export function hasPreviews(question: Question): boolean {
|
|
88
|
+
return !question.multiSelect && question.options.some((option) => Boolean(option.preview?.trim()));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Preview questions get no free-text row (notes are their free-text channel,
|
|
93
|
+
* matching Claude Code); every other question gets one. Multi-select adds an
|
|
94
|
+
* explicit "Next" to commit. "Chat about this" is always last.
|
|
95
|
+
*/
|
|
96
|
+
export function rowsFor(question: Question): Row[] {
|
|
97
|
+
const rows: Row[] = question.options.map((_, index) => ({ kind: "option", index }));
|
|
98
|
+
if (!hasPreviews(question)) rows.push({ kind: "other" });
|
|
99
|
+
if (question.multiSelect) rows.push({ kind: "next" });
|
|
100
|
+
rows.push({ kind: "chat" });
|
|
101
|
+
return rows;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Display numbers per row — the single source for both rendering and digit
|
|
106
|
+
* jumps, so a digit can only ever activate a row whose number is on screen.
|
|
107
|
+
* "Next" gets none (Claude Code leaves it unnumbered), and the preview layout
|
|
108
|
+
* shows "Chat about this" unnumbered too.
|
|
109
|
+
*/
|
|
110
|
+
export function rowNumbers(question: Question, rows: Row[]): (number | undefined)[] {
|
|
111
|
+
const unnumbered = (row: Row) => row.kind === "next" || (row.kind === "chat" && hasPreviews(question));
|
|
112
|
+
let count = 0;
|
|
113
|
+
return rows.map((row) => (unnumbered(row) ? undefined : ++count));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isAnswered(state: QuestionState): boolean {
|
|
117
|
+
return state.committed || state.selected.length > 0 || state.otherChosen;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── key handling ─────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export type WidgetKey =
|
|
123
|
+
| { kind: "up" | "down" | "prevTab" | "nextTab" | "enter" | "esc" | "backspace" }
|
|
124
|
+
| { kind: "text"; text: string };
|
|
125
|
+
|
|
126
|
+
export function decodeWidgetKey(data: string): WidgetKey | undefined {
|
|
127
|
+
switch (data) {
|
|
128
|
+
case "\x1b[A":
|
|
129
|
+
case "\x1bOA":
|
|
130
|
+
return { kind: "up" };
|
|
131
|
+
case "\x1b[B":
|
|
132
|
+
case "\x1bOB":
|
|
133
|
+
return { kind: "down" };
|
|
134
|
+
case "\x1b[D":
|
|
135
|
+
case "\x1bOD":
|
|
136
|
+
case "\x1b[Z": // shift+tab
|
|
137
|
+
return { kind: "prevTab" };
|
|
138
|
+
case "\x1b[C":
|
|
139
|
+
case "\x1bOC":
|
|
140
|
+
case "\t":
|
|
141
|
+
return { kind: "nextTab" };
|
|
142
|
+
case "\r":
|
|
143
|
+
case "\n":
|
|
144
|
+
return { kind: "enter" };
|
|
145
|
+
case "\x1b":
|
|
146
|
+
case "\x03": // ctrl+c — same intent as escape while the dialog is focused
|
|
147
|
+
return { kind: "esc" };
|
|
148
|
+
case "\x7f":
|
|
149
|
+
case "\b":
|
|
150
|
+
return { kind: "backspace" };
|
|
151
|
+
default: {
|
|
152
|
+
// Unrecognized escape sequences (home/end/F-keys) must not leak their
|
|
153
|
+
// tail characters into a text draft.
|
|
154
|
+
if (data.startsWith("\x1b")) return undefined;
|
|
155
|
+
const text = [...data].filter((char) => char >= " " && char !== "\x7f").join("");
|
|
156
|
+
return text ? { kind: "text", text } : undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Advance one key. Mutates `state`; returns a result when the dialog is done.
|
|
163
|
+
*/
|
|
164
|
+
export function applyWidgetKey(state: WidgetState, key: WidgetKey): WidgetResult | undefined {
|
|
165
|
+
if (state.editing) return applyEditingKey(state, key);
|
|
166
|
+
|
|
167
|
+
const submitTab = state.questions.length;
|
|
168
|
+
switch (key.kind) {
|
|
169
|
+
case "esc":
|
|
170
|
+
return { kind: "cancel", answers: answeredSoFar(state) };
|
|
171
|
+
case "nextTab":
|
|
172
|
+
state.tab = (state.tab + 1) % (submitTab + 1);
|
|
173
|
+
return;
|
|
174
|
+
case "prevTab":
|
|
175
|
+
state.tab = (state.tab + submitTab) % (submitTab + 1);
|
|
176
|
+
return;
|
|
177
|
+
default:
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (state.tab === submitTab) {
|
|
182
|
+
if (key.kind === "enter") {
|
|
183
|
+
const unanswered = state.questions.findIndex((_q, i) => !isAnswered(state.qs[i]));
|
|
184
|
+
if (unanswered >= 0) {
|
|
185
|
+
state.tab = unanswered;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
return { kind: "submit", answers: collectAnswers(state) };
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const question = state.questions[state.tab];
|
|
194
|
+
const questionState = state.qs[state.tab];
|
|
195
|
+
const rows = rowsFor(question);
|
|
196
|
+
|
|
197
|
+
switch (key.kind) {
|
|
198
|
+
case "up":
|
|
199
|
+
questionState.cursor = Math.max(0, questionState.cursor - 1);
|
|
200
|
+
return;
|
|
201
|
+
case "down":
|
|
202
|
+
questionState.cursor = Math.min(rows.length - 1, questionState.cursor + 1);
|
|
203
|
+
return;
|
|
204
|
+
case "enter":
|
|
205
|
+
return activateRow(state, rows[questionState.cursor]);
|
|
206
|
+
case "text": {
|
|
207
|
+
if (key.text === "n" && hasPreviews(question)) {
|
|
208
|
+
state.editing = { field: "notes", draft: questionState.notes };
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const digit = Number(key.text);
|
|
212
|
+
const target = rowNumbers(question, rows).indexOf(digit);
|
|
213
|
+
if (target >= 0) {
|
|
214
|
+
questionState.cursor = target;
|
|
215
|
+
return activateRow(state, rows[target]);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
default:
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function activateRow(state: WidgetState, row: Row): WidgetResult | undefined {
|
|
225
|
+
const question = state.questions[state.tab];
|
|
226
|
+
const questionState = state.qs[state.tab];
|
|
227
|
+
switch (row.kind) {
|
|
228
|
+
case "option": {
|
|
229
|
+
if (question.multiSelect) {
|
|
230
|
+
const at = questionState.selected.indexOf(row.index);
|
|
231
|
+
if (at >= 0) questionState.selected.splice(at, 1);
|
|
232
|
+
else questionState.selected.push(row.index);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
questionState.selected = [row.index];
|
|
236
|
+
questionState.otherChosen = false;
|
|
237
|
+
advance(state);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
case "other":
|
|
241
|
+
state.editing = { field: "other", draft: questionState.otherText };
|
|
242
|
+
return;
|
|
243
|
+
case "next":
|
|
244
|
+
questionState.committed = true;
|
|
245
|
+
advance(state);
|
|
246
|
+
return;
|
|
247
|
+
case "chat":
|
|
248
|
+
return { kind: "chat" };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function applyEditingKey(state: WidgetState, key: WidgetKey): WidgetResult | undefined {
|
|
253
|
+
const editing = state.editing;
|
|
254
|
+
if (!editing) return;
|
|
255
|
+
const question = state.questions[state.tab];
|
|
256
|
+
const questionState = state.qs[state.tab];
|
|
257
|
+
|
|
258
|
+
switch (key.kind) {
|
|
259
|
+
case "esc":
|
|
260
|
+
state.editing = undefined;
|
|
261
|
+
return;
|
|
262
|
+
case "backspace":
|
|
263
|
+
editing.draft = editing.draft.slice(0, -1);
|
|
264
|
+
return;
|
|
265
|
+
case "text":
|
|
266
|
+
editing.draft += key.text;
|
|
267
|
+
return;
|
|
268
|
+
case "enter": {
|
|
269
|
+
state.editing = undefined;
|
|
270
|
+
if (editing.field === "notes") {
|
|
271
|
+
questionState.notes = editing.draft.trim();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
questionState.otherText = editing.draft.trim();
|
|
275
|
+
questionState.otherChosen = questionState.otherText !== "";
|
|
276
|
+
// A typed answer on a single-select question IS the answer.
|
|
277
|
+
if (questionState.otherChosen && !question.multiSelect) {
|
|
278
|
+
questionState.selected = [];
|
|
279
|
+
advance(state);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
default:
|
|
284
|
+
return; // arrows/tab are inert while typing
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Move to the next unanswered question, or the Submit tab when none remain. */
|
|
289
|
+
function advance(state: WidgetState): void {
|
|
290
|
+
const count = state.questions.length;
|
|
291
|
+
for (let step = 1; step < count; step++) {
|
|
292
|
+
const i = (state.tab + step) % count;
|
|
293
|
+
if (!isAnswered(state.qs[i])) {
|
|
294
|
+
state.tab = i;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
state.tab = count;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── answers ──────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
function collectAnswer(question: Question, state: QuestionState): Answer {
|
|
304
|
+
const labels = [...state.selected].sort((a, b) => a - b).map((index) => question.options[index].label);
|
|
305
|
+
if (state.otherChosen) labels.push(state.otherText);
|
|
306
|
+
return {
|
|
307
|
+
question: question.question,
|
|
308
|
+
header: question.header,
|
|
309
|
+
selected: labels,
|
|
310
|
+
freeform: labels.length > 0 && state.selected.length === 0,
|
|
311
|
+
notes: state.notes || undefined,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function collectAnswers(state: WidgetState): Answer[] {
|
|
316
|
+
return state.questions.map((question, i) => collectAnswer(question, state.qs[i]));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Answers for the questions already answered — what an Esc abandons. */
|
|
320
|
+
function answeredSoFar(state: WidgetState): Answer[] {
|
|
321
|
+
return state.questions.flatMap((question, i) =>
|
|
322
|
+
isAnswered(state.qs[i]) ? [collectAnswer(question, state.qs[i])] : [],
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── rendering ────────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
export function renderWidget(state: WidgetState, style: WidgetStyle, width: number): string[] {
|
|
329
|
+
const { paint } = style;
|
|
330
|
+
const out: string[] = [paint("dim", "─".repeat(Math.max(0, width)))];
|
|
331
|
+
out.push(renderTabBar(state, style));
|
|
332
|
+
out.push("");
|
|
333
|
+
if (state.tab === state.questions.length) renderSubmitTab(state, paint, width, out);
|
|
334
|
+
else renderQuestionTab(state, style, width, out);
|
|
335
|
+
out.push("");
|
|
336
|
+
out.push(paint("dim", cutPlainText(footerFor(state), Math.max(1, width - 1))));
|
|
337
|
+
// Final ANSI-aware guard: pi-tui crashes the app on an overwide line, and
|
|
338
|
+
// the painted tab bar can exceed narrow widths.
|
|
339
|
+
return out.map((line) => truncateLine(line, width));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Active tab is an accent chip (reverse video), like Claude Code's. */
|
|
343
|
+
function renderTabBar(state: WidgetState, style: WidgetStyle): string {
|
|
344
|
+
const { paint, inverse } = style;
|
|
345
|
+
const chip = (text: string, active: boolean) => (active ? inverse(paint("accent", ` ${text} `)) : ` ${text} `);
|
|
346
|
+
const segments = state.questions.map((question, i) => {
|
|
347
|
+
const glyph = isAnswered(state.qs[i]) ? TAB_ANSWERED : TAB_UNANSWERED;
|
|
348
|
+
return chip(`${glyph} ${question.header}`, i === state.tab);
|
|
349
|
+
});
|
|
350
|
+
segments.push(chip("✔ Submit", state.tab === state.questions.length));
|
|
351
|
+
return `${paint("dim", "←")} ${segments.join(" ")} ${paint("dim", "→")}`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function footerFor(state: WidgetState): string {
|
|
355
|
+
if (state.editing) return "Enter to confirm · Esc to back out";
|
|
356
|
+
if (state.tab === state.questions.length) return "Enter to submit · Tab/shift+Tab to switch questions · Esc to cancel";
|
|
357
|
+
// The preview layout spells out ↑/↓ and the notes key; the stacked layout
|
|
358
|
+
// uses Claude Code's shorter wording.
|
|
359
|
+
if (hasPreviews(state.questions[state.tab]))
|
|
360
|
+
return "Enter to select · ↑/↓ to navigate · n to add notes · Tab/shift+Tab to switch questions · Esc to cancel";
|
|
361
|
+
return "Enter to select · Tab/shift+Tab or arrow keys to navigate · Esc to cancel";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderSubmitTab(state: WidgetState, paint: Paint, width: number, out: string[]): void {
|
|
365
|
+
const cut = (line: string) => cutPlainText(line, Math.max(1, width - 1));
|
|
366
|
+
for (const [i, question] of state.questions.entries()) {
|
|
367
|
+
const questionState = state.qs[i];
|
|
368
|
+
if (isAnswered(questionState)) {
|
|
369
|
+
const answer = collectAnswer(question, questionState);
|
|
370
|
+
const value = answer.selected.length > 0 ? answer.selected.join(", ") : "(nothing selected)";
|
|
371
|
+
out.push(cut(`· ${question.question} → ${value}`));
|
|
372
|
+
} else {
|
|
373
|
+
out.push(paint("dim", cut(`· ${question.question} → (unanswered)`)));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const ready = state.questions.every((_q, i) => isAnswered(state.qs[i]));
|
|
377
|
+
out.push("");
|
|
378
|
+
out.push(
|
|
379
|
+
ready
|
|
380
|
+
? paint("accent", cut("All questions answered — press Enter to submit."))
|
|
381
|
+
: paint("dim", cut("Some questions are unanswered — Enter jumps to the first one.")),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function renderQuestionTab(state: WidgetState, style: WidgetStyle, width: number, out: string[]): void {
|
|
386
|
+
const question = state.questions[state.tab];
|
|
387
|
+
for (const line of wrapPlainText(question.question, Math.max(10, width - 1))) out.push(style.bold(line));
|
|
388
|
+
out.push("");
|
|
389
|
+
if (hasPreviews(question)) renderPreviewRows(state, style, width, out);
|
|
390
|
+
else renderStackedRows(state, style, width, out);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** `❯` on the focused row; selection and focus both paint accent. */
|
|
394
|
+
function pointerFor(state: WidgetState, rowIndex: number): string {
|
|
395
|
+
return !state.editing && state.qs[state.tab].cursor === rowIndex ? "❯" : " ";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** The divider + "Chat about this" row both layouts end with (always the last row). */
|
|
399
|
+
function pushChatRow(state: WidgetState, rows: Row[], style: WidgetStyle, width: number, out: string[]): void {
|
|
400
|
+
const { paint, bold } = style;
|
|
401
|
+
const question = state.questions[state.tab];
|
|
402
|
+
const rowIndex = rows.length - 1;
|
|
403
|
+
const number = rowNumbers(question, rows)[rowIndex];
|
|
404
|
+
const label = number === undefined ? CHAT_LABEL : `${number}. ${CHAT_LABEL}`;
|
|
405
|
+
out.push(paint("dim", "─".repeat(Math.max(0, width - 1))));
|
|
406
|
+
const line = bold(cutPlainText(`${pointerFor(state, rowIndex)} ${label}`, Math.max(1, width - 1)));
|
|
407
|
+
const focused = !state.editing && state.qs[state.tab].cursor === rowIndex;
|
|
408
|
+
out.push(focused ? paint("accent", line) : line);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function renderStackedRows(state: WidgetState, style: WidgetStyle, width: number, out: string[]): void {
|
|
412
|
+
const { paint, bold } = style;
|
|
413
|
+
const question = state.questions[state.tab];
|
|
414
|
+
const questionState = state.qs[state.tab];
|
|
415
|
+
const rows = rowsFor(question);
|
|
416
|
+
const numbers = rowNumbers(question, rows);
|
|
417
|
+
const cut = (line: string) => cutPlainText(line, Math.max(1, width - 1));
|
|
418
|
+
// Claude Code's look: labels bold, descriptions muted underneath, the whole
|
|
419
|
+
// focused/selected row accent on top of that. Overflow is handled by
|
|
420
|
+
// renderWidget's final truncateLine pass.
|
|
421
|
+
const labelRow = (prefix: string, label: string, highlight: boolean) => {
|
|
422
|
+
const line = `${prefix}${bold(label)}`;
|
|
423
|
+
return highlight ? paint("accent", line) : line;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
for (const [r, row] of rows.entries()) {
|
|
427
|
+
const focused = !state.editing && questionState.cursor === r;
|
|
428
|
+
const pointer = pointerFor(state, r);
|
|
429
|
+
const number = `${numbers[r]}.`;
|
|
430
|
+
switch (row.kind) {
|
|
431
|
+
case "option": {
|
|
432
|
+
const option = question.options[row.index];
|
|
433
|
+
const check = question.multiSelect ? (questionState.selected.includes(row.index) ? "[✔] " : "[ ] ") : "";
|
|
434
|
+
const selected = questionState.selected.includes(row.index);
|
|
435
|
+
out.push(labelRow(`${pointer} ${number} ${check}`, option.label, focused || selected));
|
|
436
|
+
if (option.description) out.push(paint("muted", cut(` ${" ".repeat(number.length)}${option.description}`)));
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
case "other": {
|
|
440
|
+
const editing = state.editing?.field === "other";
|
|
441
|
+
const check = question.multiSelect ? (questionState.otherChosen ? "[✔] " : "[ ] ") : "";
|
|
442
|
+
if (editing) {
|
|
443
|
+
out.push(paint("accent", cut(`❯ ${number} ${check}${state.editing?.draft ?? ""}▏`)));
|
|
444
|
+
} else if (questionState.otherText) {
|
|
445
|
+
out.push(labelRow(`${pointer} ${number} ${check}`, questionState.otherText, focused || questionState.otherChosen));
|
|
446
|
+
} else {
|
|
447
|
+
out.push(labelRow(`${pointer} ${number} ${check}`, OTHER_PLACEHOLDER, focused));
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
case "next": {
|
|
452
|
+
const line = bold(cut(`${pointer} Next`));
|
|
453
|
+
out.push(focused ? paint("accent", line) : line);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
case "chat":
|
|
457
|
+
pushChatRow(state, rows, style, width, out);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function renderPreviewRows(state: WidgetState, style: WidgetStyle, width: number, out: string[]): void {
|
|
464
|
+
const { paint } = style;
|
|
465
|
+
const question = state.questions[state.tab];
|
|
466
|
+
const questionState = state.qs[state.tab];
|
|
467
|
+
const rows = rowsFor(question);
|
|
468
|
+
const numbers = rowNumbers(question, rows);
|
|
469
|
+
const cut = (line: string) => cutPlainText(line, Math.max(1, width - 1));
|
|
470
|
+
|
|
471
|
+
// Left column: numbered option labels, padded plain before painting so the
|
|
472
|
+
// ANSI escapes never enter the width math.
|
|
473
|
+
const leftPlain = question.options.map((option, i) => `${pointerFor(state, i)} ${numbers[i]}. ${option.label}`);
|
|
474
|
+
const leftWidth = Math.min(
|
|
475
|
+
Math.max(...leftPlain.map((line) => visibleWidth(line))) + 2,
|
|
476
|
+
Math.max(12, Math.floor(width * 0.45)),
|
|
477
|
+
);
|
|
478
|
+
const leftLines = leftPlain.map((line, i) => {
|
|
479
|
+
const padded = padPlainText(line, leftWidth);
|
|
480
|
+
const focused = !state.editing && questionState.cursor === i;
|
|
481
|
+
return focused || questionState.selected.includes(i) ? paint("accent", padded) : padded;
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// The preview follows the focused option (falling back to the selection).
|
|
485
|
+
const focusIndex =
|
|
486
|
+
questionState.cursor < question.options.length ? questionState.cursor : (questionState.selected[0] ?? 0);
|
|
487
|
+
const preview = question.options[focusIndex]?.preview?.trim();
|
|
488
|
+
const boxWidth = Math.max(0, width - leftWidth - 2);
|
|
489
|
+
const sideBySide = boxWidth >= 18;
|
|
490
|
+
const box = preview ? previewBox(preview, sideBySide ? boxWidth : Math.max(18, width - 3), paint) : [];
|
|
491
|
+
|
|
492
|
+
if (sideBySide) {
|
|
493
|
+
for (let i = 0; i < Math.max(leftLines.length, box.length); i++) {
|
|
494
|
+
out.push(`${leftLines[i] ?? " ".repeat(leftWidth)}${box[i] ?? ""}`);
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
out.push(...leftLines.map((line) => line.trimEnd()));
|
|
498
|
+
if (box.length > 0) {
|
|
499
|
+
out.push("");
|
|
500
|
+
out.push(...box.map((line) => ` ${line}`));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Notes sit under the preview, aligned with the box when side by side.
|
|
505
|
+
const notesIndent = " ".repeat(sideBySide ? leftWidth : 2);
|
|
506
|
+
out.push("");
|
|
507
|
+
if (state.editing?.field === "notes") {
|
|
508
|
+
out.push(`${notesIndent}${paint("accent", "Notes: ")}${cut(state.editing.draft)}▏`);
|
|
509
|
+
} else if (questionState.notes) {
|
|
510
|
+
out.push(`${notesIndent}${paint("accent", "Notes: ")}${cut(questionState.notes)}`);
|
|
511
|
+
} else {
|
|
512
|
+
out.push(`${notesIndent}${paint("accent", "Notes: ")}${paint("dim", NOTES_PLACEHOLDER)}`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
out.push("");
|
|
516
|
+
pushChatRow(state, rows, style, width, out);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function previewBox(preview: string, maxOuter: number, paint: Paint): string[] {
|
|
520
|
+
const inner = Math.max(8, maxOuter - 4);
|
|
521
|
+
const wrapped = wrapPlainText(preview.replace(/\t/g, " "), inner);
|
|
522
|
+
const shown = wrapped.slice(0, MAX_PREVIEW_ROWS);
|
|
523
|
+
const hidden = wrapped.length - shown.length;
|
|
524
|
+
const contentWidth = Math.min(inner, Math.max(8, ...shown.map((line) => visibleWidth(line))));
|
|
525
|
+
const border = paint("dim", "│");
|
|
526
|
+
const body = shown.map((line) => `${border} ${padPlainText(line, contentWidth)} ${border}`);
|
|
527
|
+
if (hidden > 0) body.push(`${border} ${paint("dim", padPlainText(`… +${hidden} more lines`, contentWidth))} ${border}`);
|
|
528
|
+
return [
|
|
529
|
+
paint("dim", `┌${"─".repeat(contentWidth + 2)}┐`),
|
|
530
|
+
...body,
|
|
531
|
+
paint("dim", `└${"─".repeat(contentWidth + 2)}┘`),
|
|
532
|
+
];
|
|
533
|
+
}
|
|
@@ -37,6 +37,15 @@ export interface SubagentActionsPayload {
|
|
|
37
37
|
background?: boolean;
|
|
38
38
|
/** Agent name, used to label a background review. */
|
|
39
39
|
agentName?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Background only: the gate answers here exactly once — the rendered flag
|
|
42
|
+
* text when the review raised a concern, undefined when the sequence is
|
|
43
|
+
* fine or auto mode is off — so the emitter can deliver the verdict with
|
|
44
|
+
* the completion report rather than as a trailing notice. Answered
|
|
45
|
+
* synchronously when no review will run, so the emitter never waits for
|
|
46
|
+
* nothing.
|
|
47
|
+
*/
|
|
48
|
+
onReview?: (flag: string | undefined) => void;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
/** Per-action and total caps: a long-running child must not blow up the prompt. */
|
|
@@ -50,29 +59,3 @@ export function recordAction(log: ChildAction[], toolName: string, input: unknow
|
|
|
50
59
|
const subject = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
51
60
|
log.push({ toolName, subject: subject.slice(0, SUBJECT_LIMIT) });
|
|
52
61
|
}
|
|
53
|
-
|
|
54
|
-
/** Render the log for the classifier, collapsing repeats so 40 reads read as one line. */
|
|
55
|
-
export function renderActions(actions: ChildAction[]): string {
|
|
56
|
-
const lines: string[] = [];
|
|
57
|
-
let previous: string | undefined;
|
|
58
|
-
let repeats = 0;
|
|
59
|
-
|
|
60
|
-
const flush = () => {
|
|
61
|
-
if (previous === undefined) return;
|
|
62
|
-
lines.push(repeats > 1 ? `${previous} (×${repeats})` : previous);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
for (const action of actions) {
|
|
66
|
-
const line = action.subject ? `${action.toolName}: ${action.subject}` : action.toolName;
|
|
67
|
-
if (line === previous) {
|
|
68
|
-
repeats++;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
flush();
|
|
72
|
-
previous = line;
|
|
73
|
-
repeats = 1;
|
|
74
|
-
}
|
|
75
|
-
flush();
|
|
76
|
-
|
|
77
|
-
return lines.join("\n");
|
|
78
|
-
}
|