@bastani/atomic 0.8.4 → 0.8.5
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/CHANGELOG.md +12 -0
- package/README.md +24 -23
- package/dist/builtin/intercom/README.md +5 -5
- package/dist/builtin/intercom/index.ts +1 -1
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/ui/compose.ts +19 -1
- package/dist/builtin/intercom/ui/session-list.ts +19 -1
- package/dist/builtin/mcp/README.md +3 -3
- package/dist/builtin/mcp/commands.ts +1 -1
- package/dist/builtin/mcp/host-html-template.ts +1 -1
- package/dist/builtin/mcp/mcp-panel.ts +14 -14
- package/dist/builtin/mcp/mcp-setup-panel.ts +4 -4
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/mcp/tool-result-renderer.ts +1 -1
- package/dist/builtin/subagents/README.md +3 -3
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/tui/render.ts +1844 -1062
- package/dist/builtin/web-access/README.md +1 -1
- package/dist/builtin/web-access/curator-page.ts +2 -2
- package/dist/builtin/web-access/index.ts +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/README.md +34 -7
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +23 -4
- package/dist/builtin/workflows/builtin/ralph.ts +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/workflow/SKILL.md +75 -16
- package/dist/builtin/workflows/skills/workflow/references/running-workflows.md +34 -11
- package/dist/builtin/workflows/skills/workflow/references/sdk-authoring.md +111 -20
- package/dist/builtin/workflows/src/extension/discovery.ts +32 -4
- package/dist/builtin/workflows/src/extension/index.ts +347 -63
- package/dist/builtin/workflows/src/extension/render-call.ts +3 -1
- package/dist/builtin/workflows/src/extension/render-result.ts +7 -0
- package/dist/builtin/workflows/src/extension/runtime.ts +4 -2
- package/dist/builtin/workflows/src/extension/wiring.ts +32 -8
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +36 -14
- package/dist/builtin/workflows/src/runs/background/runner.ts +2 -2
- package/dist/builtin/workflows/src/runs/background/status.ts +89 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +338 -78
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +2 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +55 -7
- package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +146 -10
- package/dist/builtin/workflows/src/shared/store.ts +29 -0
- package/dist/builtin/workflows/src/shared/types.ts +25 -4
- package/dist/builtin/workflows/src/tui/graph-canvas.ts +69 -2
- package/dist/builtin/workflows/src/tui/graph-view.ts +97 -182
- package/dist/builtin/workflows/src/tui/header.ts +36 -20
- package/dist/builtin/workflows/src/tui/inline-form-card.ts +129 -46
- package/dist/builtin/workflows/src/tui/inline-form-editor.ts +111 -36
- package/dist/builtin/workflows/src/tui/inputs-picker.ts +311 -91
- package/dist/builtin/workflows/src/tui/layout.ts +1 -1
- package/dist/builtin/workflows/src/tui/node-card.ts +66 -37
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +20 -6
- package/dist/builtin/workflows/src/tui/prompt-card.ts +262 -85
- package/dist/builtin/workflows/src/tui/run-detail.ts +50 -31
- package/dist/builtin/workflows/src/tui/session-confirm.ts +21 -14
- package/dist/builtin/workflows/src/tui/session-picker.ts +35 -26
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +531 -960
- package/dist/builtin/workflows/src/tui/status-helpers.ts +6 -0
- package/dist/builtin/workflows/src/tui/status-list.ts +8 -4
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +7 -2
- package/dist/builtin/workflows/src/tui/switcher.ts +55 -25
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +33 -1
- package/dist/builtin/workflows/src/tui/workflow-list.ts +10 -6
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -6
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -3
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +7 -7
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +2 -2
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +3 -2
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +24 -12
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +6 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +28 -17
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/package-manager.d.ts +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +65 -28
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +13 -5
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +5 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts +1 -1
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js +1 -1
- package/dist/core/tools/ask-user-question/view/components/preview/preview-block-renderer.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts +8 -8
- package/dist/core/tools/ask-user-question/view/dialog-builder.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/dialog-builder.js +6 -6
- package/dist/core/tools/ask-user-question/view/dialog-builder.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +1 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +7 -4
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +3 -2
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +3 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +2 -2
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +2 -1
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/todos.d.ts.map +1 -1
- package/dist/core/tools/todos.js +1 -1
- package/dist/core/tools/todos.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts +4 -3
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +1 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +3 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +3 -3
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +2 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +3 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +13 -3
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +2 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +2 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +1 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +47 -5
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +5 -5
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +3 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +7 -7
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +8 -8
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +3 -3
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +10 -12
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +3 -3
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/working-status.d.ts +25 -0
- package/dist/modes/interactive/components/working-status.d.ts.map +1 -0
- package/dist/modes/interactive/components/working-status.js +28 -0
- package/dist/modes/interactive/components/working-status.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +8 -7
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +8 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +5 -5
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/development.md +2 -2
- package/docs/extensions.md +7 -7
- package/docs/packages.md +11 -8
- package/docs/quickstart.md +2 -2
- package/docs/rpc.md +1 -1
- package/docs/sdk.md +14 -11
- package/docs/session-format.md +1 -1
- package/docs/sessions.md +10 -10
- package/docs/settings.md +1 -1
- package/docs/terminal-setup.md +9 -9
- package/docs/tmux.md +10 -10
- package/docs/tui.md +2 -2
- package/docs/usage.md +9 -9
- package/package.json +6 -1
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* ╰──────────────────────────────────────────────────────────────────╯
|
|
26
26
|
* integer · optional · loop count
|
|
27
27
|
*
|
|
28
|
-
* ╭ EDIT ╮ tab next · shift+tab prev · ctrl+
|
|
28
|
+
* ╭ EDIT ╮ tab next · shift+tab prev · ctrl+enter run · esc cancel
|
|
29
29
|
* │ EDIT │
|
|
30
30
|
* ╰──────╯
|
|
31
31
|
*
|
|
@@ -55,6 +55,55 @@ export interface InlineCardOpts {
|
|
|
55
55
|
theme: GraphTheme;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
59
|
+
|
|
60
|
+
function graphemes(text: string): string[] {
|
|
61
|
+
return Array.from(graphemeSegmenter.segment(text), (s) => s.segment);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function clampGraphemeOffset(text: string, caret: number): number {
|
|
65
|
+
const c = Math.max(0, Math.min(caret, text.length));
|
|
66
|
+
if (c === text.length) return c;
|
|
67
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
68
|
+
if (s.index === c) return c;
|
|
69
|
+
if (s.index > c) break;
|
|
70
|
+
}
|
|
71
|
+
let prev = 0;
|
|
72
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
73
|
+
if (s.index >= c) break;
|
|
74
|
+
prev = s.index;
|
|
75
|
+
}
|
|
76
|
+
return prev;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function headToWidth(text: string, width: number): string {
|
|
80
|
+
if (width <= 0) return "";
|
|
81
|
+
let out = "";
|
|
82
|
+
let used = 0;
|
|
83
|
+
for (const g of graphemes(text)) {
|
|
84
|
+
const w = visibleWidth(g);
|
|
85
|
+
if (used + w > width) break;
|
|
86
|
+
out += g;
|
|
87
|
+
used += w;
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function tailToWidth(text: string, width: number): string {
|
|
93
|
+
if (width <= 0) return "";
|
|
94
|
+
let out = "";
|
|
95
|
+
let used = 0;
|
|
96
|
+
const gs = graphemes(text);
|
|
97
|
+
for (let i = gs.length - 1; i >= 0; i--) {
|
|
98
|
+
const g = gs[i]!;
|
|
99
|
+
const w = visibleWidth(g);
|
|
100
|
+
if (used + w > width) break;
|
|
101
|
+
out = g + out;
|
|
102
|
+
used += w;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
58
107
|
// ---------------------------------------------------------------------------
|
|
59
108
|
// Public renderer
|
|
60
109
|
// ---------------------------------------------------------------------------
|
|
@@ -110,7 +159,8 @@ function renderHeaderBand(state: InlineFormState, theme: GraphTheme, width: numb
|
|
|
110
159
|
);
|
|
111
160
|
|
|
112
161
|
const nameVisible = ` ${state.workflowName}`;
|
|
113
|
-
const
|
|
162
|
+
const focusTargetCount = state.fields.length;
|
|
163
|
+
const counter = `${Math.min(state.focusedIdx + 1, focusTargetCount)} / ${focusTargetCount}`;
|
|
114
164
|
const counterVisible = counter;
|
|
115
165
|
|
|
116
166
|
const leftEdgePad = 1;
|
|
@@ -130,8 +180,6 @@ function renderHeaderBand(state: InlineFormState, theme: GraphTheme, width: numb
|
|
|
130
180
|
|
|
131
181
|
function renderFooterBand(theme: GraphTheme, width: number): string[] {
|
|
132
182
|
const chromeBg = hexBg(theme.backgroundPanel);
|
|
133
|
-
const text = hexToAnsi(theme.text);
|
|
134
|
-
const muted = hexToAnsi(theme.textMuted);
|
|
135
183
|
const dim = hexToAnsi(theme.dim);
|
|
136
184
|
|
|
137
185
|
const { top, mid, bot, visibleWidth: pillW } = renderOutlinePill(
|
|
@@ -140,11 +188,13 @@ function renderFooterBand(theme: GraphTheme, width: number): string[] {
|
|
|
140
188
|
chromeBg,
|
|
141
189
|
);
|
|
142
190
|
|
|
191
|
+
const text = hexToAnsi(theme.text);
|
|
192
|
+
const muted = hexToAnsi(theme.textMuted);
|
|
143
193
|
const hints: Array<{ key: string; label: string }> = [
|
|
144
|
-
{ key: "tab", label: "
|
|
145
|
-
{ key: "shift+tab", label: "
|
|
146
|
-
{ key: "ctrl+
|
|
147
|
-
{ key: "esc", label: "
|
|
194
|
+
{ key: "tab", label: "Next" },
|
|
195
|
+
{ key: "shift+tab", label: "Prev" },
|
|
196
|
+
{ key: "ctrl+enter", label: "Run" },
|
|
197
|
+
{ key: "esc", label: "Cancel" },
|
|
148
198
|
];
|
|
149
199
|
const sep = `${chromeBg} ${dim}·${RESET}${chromeBg} `;
|
|
150
200
|
const segments = hints.map(
|
|
@@ -280,15 +330,7 @@ function renderFieldContent(
|
|
|
280
330
|
return [paint(field.placeholder ?? "", theme.dim)];
|
|
281
331
|
}
|
|
282
332
|
if (focused) {
|
|
283
|
-
|
|
284
|
-
const before = raw.slice(0, c);
|
|
285
|
-
const after = raw.slice(c);
|
|
286
|
-
return [
|
|
287
|
-
clip(
|
|
288
|
-
paint(before, theme.text) + paint("▋", theme.accent) + paint(after, theme.text),
|
|
289
|
-
usable,
|
|
290
|
-
),
|
|
291
|
-
];
|
|
333
|
+
return [renderCaretLine(raw, caret ?? raw.length, usable, theme, theme.text)];
|
|
292
334
|
}
|
|
293
335
|
return [clip(paint(raw, theme.textMuted), usable)];
|
|
294
336
|
}
|
|
@@ -309,12 +351,30 @@ function renderFieldContent(
|
|
|
309
351
|
if (row !== layout.cursorRow) {
|
|
310
352
|
return paint(line, theme.text);
|
|
311
353
|
}
|
|
312
|
-
|
|
313
|
-
const after = line.slice(layout.cursorCol);
|
|
314
|
-
return paint(before, theme.text) + paint("▋", theme.accent) + paint(after, theme.text);
|
|
354
|
+
return renderCaretLine(line, layout.cursorOffset ?? line.length, usable, theme, theme.text);
|
|
315
355
|
});
|
|
316
356
|
}
|
|
317
357
|
|
|
358
|
+
function renderCaretLine(
|
|
359
|
+
raw: string,
|
|
360
|
+
caret: number,
|
|
361
|
+
usable: number,
|
|
362
|
+
theme: GraphTheme,
|
|
363
|
+
color: string,
|
|
364
|
+
): string {
|
|
365
|
+
const safe = clampGraphemeOffset(raw, caret);
|
|
366
|
+
const beforeFull = raw.slice(0, safe);
|
|
367
|
+
const afterFull = raw.slice(safe);
|
|
368
|
+
const cursorWidth = 1;
|
|
369
|
+
let before = beforeFull;
|
|
370
|
+
let after = afterFull;
|
|
371
|
+
if (visibleWidth(beforeFull) + cursorWidth + visibleWidth(afterFull) > usable) {
|
|
372
|
+
before = tailToWidth(beforeFull, Math.max(0, usable - cursorWidth));
|
|
373
|
+
after = headToWidth(afterFull, Math.max(0, usable - visibleWidth(before) - cursorWidth));
|
|
374
|
+
}
|
|
375
|
+
return clip(paint(before, color) + paint("▋", theme.accent) + paint(after, color), usable);
|
|
376
|
+
}
|
|
377
|
+
|
|
318
378
|
// ---------------------------------------------------------------------------
|
|
319
379
|
// Frozen states
|
|
320
380
|
// ---------------------------------------------------------------------------
|
|
@@ -382,40 +442,63 @@ export function layoutTextField(
|
|
|
382
442
|
raw: string,
|
|
383
443
|
usable: number,
|
|
384
444
|
caret: number,
|
|
385
|
-
): { lines: string[]; cursorRow: number; cursorCol: number } {
|
|
445
|
+
): { lines: string[]; cursorRow: number; cursorCol: number; cursorOffset?: number } {
|
|
386
446
|
const width = Math.max(1, Math.floor(usable));
|
|
387
|
-
const safeCaret =
|
|
447
|
+
const safeCaret = clampGraphemeOffset(raw, caret);
|
|
388
448
|
const visualLines: string[] = [];
|
|
449
|
+
const lineStarts: number[] = [];
|
|
450
|
+
const lineEnds: number[] = [];
|
|
389
451
|
let curLine = "";
|
|
390
|
-
let
|
|
391
|
-
let
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
452
|
+
let curWidth = 0;
|
|
453
|
+
let lineStart = 0;
|
|
454
|
+
|
|
455
|
+
const pushLine = (end: number): void => {
|
|
456
|
+
visualLines.push(curLine);
|
|
457
|
+
lineStarts.push(lineStart);
|
|
458
|
+
lineEnds.push(end);
|
|
459
|
+
curLine = "";
|
|
460
|
+
curWidth = 0;
|
|
461
|
+
lineStart = end;
|
|
399
462
|
};
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
463
|
+
|
|
464
|
+
for (const s of graphemeSegmenter.segment(raw)) {
|
|
465
|
+
const offset = s.index;
|
|
466
|
+
const g = s.segment;
|
|
467
|
+
if (g === "\n") {
|
|
468
|
+
pushLine(offset);
|
|
469
|
+
lineStart = offset + g.length;
|
|
406
470
|
continue;
|
|
407
471
|
}
|
|
408
|
-
|
|
409
|
-
if (curLine
|
|
410
|
-
|
|
411
|
-
|
|
472
|
+
const w = visibleWidth(g);
|
|
473
|
+
if (curLine !== "" && curWidth + w > width) {
|
|
474
|
+
pushLine(offset);
|
|
475
|
+
}
|
|
476
|
+
curLine += g;
|
|
477
|
+
curWidth += w;
|
|
478
|
+
if (curWidth >= width) {
|
|
479
|
+
pushLine(offset + g.length);
|
|
412
480
|
}
|
|
413
481
|
}
|
|
414
|
-
recordCursorIfMatched(raw.length);
|
|
415
482
|
visualLines.push(curLine);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
483
|
+
lineStarts.push(lineStart);
|
|
484
|
+
lineEnds.push(raw.length);
|
|
485
|
+
|
|
486
|
+
let cursorRow = visualLines.length - 1;
|
|
487
|
+
for (let i = 0; i < visualLines.length; i++) {
|
|
488
|
+
const start = lineStarts[i]!;
|
|
489
|
+
const end = lineEnds[i]!;
|
|
490
|
+
const nextStart = lineStarts[i + 1];
|
|
491
|
+
if (safeCaret >= start && safeCaret < end) {
|
|
492
|
+
cursorRow = i;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
if (safeCaret === end) {
|
|
496
|
+
cursorRow = nextStart === safeCaret ? i + 1 : i;
|
|
497
|
+
}
|
|
419
498
|
}
|
|
420
|
-
|
|
499
|
+
cursorRow = Math.max(0, Math.min(cursorRow, visualLines.length - 1));
|
|
500
|
+
const line = visualLines[cursorRow] ?? "";
|
|
501
|
+
const cursorOffset = Math.max(0, Math.min(safeCaret - (lineStarts[cursorRow] ?? 0), line.length));
|
|
502
|
+
const cursorCol = visibleWidth(line.slice(0, cursorOffset));
|
|
503
|
+
return { lines: visualLines, cursorRow, cursorCol, cursorOffset };
|
|
421
504
|
}
|
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
* ctrl+u — delete to logical line start
|
|
15
15
|
* ctrl+k — delete to logical line end
|
|
16
16
|
* space — boolean toggle
|
|
17
|
-
* enter — newline (text) |
|
|
17
|
+
* enter — newline (text) | otherwise next field
|
|
18
18
|
* printable ASCII — insert at caret (text/string/number)
|
|
19
|
-
* ctrl+
|
|
20
|
-
* esc
|
|
19
|
+
* ctrl+enter — submit form (if valid)
|
|
20
|
+
* esc / ctrl+c — cancel form
|
|
21
21
|
*
|
|
22
22
|
* Editor-mode keys (cursor movement, word jumps, deletions) route through
|
|
23
23
|
* the Pi `KeybindingsManager` injected by the host at factory time, so any
|
|
24
24
|
* user-configured keybinding overrides surfaces here as well. Form-level
|
|
25
|
-
* keys (tab/shift+tab/esc/ctrl+
|
|
26
|
-
* workflow form contract, not Pi-configurable actions.
|
|
25
|
+
* keys (tab/shift+tab/ctrl+enter/esc/ctrl+c) stay as raw byte checks because
|
|
26
|
+
* they are workflow form contract, not Pi-configurable actions.
|
|
27
27
|
*
|
|
28
28
|
* On submit/cancel the editor calls back to the orchestrator which:
|
|
29
29
|
* 1. Marks the form state finalized (renderer flips to frozen view)
|
|
@@ -57,13 +57,14 @@ import {
|
|
|
57
57
|
wordLeft,
|
|
58
58
|
wordRight,
|
|
59
59
|
} from "./keybindings-adapter.js";
|
|
60
|
+
import { matchesKey, visibleWidth } from "./text-helpers.js";
|
|
60
61
|
|
|
61
62
|
export type FormEditorOutcome = "submit" | "cancel";
|
|
62
63
|
|
|
63
64
|
export interface InlineFormEditorOpts {
|
|
64
65
|
formId: string;
|
|
65
66
|
theme: GraphTheme;
|
|
66
|
-
/** Called when
|
|
67
|
+
/** Called when Ctrl+Enter passes validation or cancel fires. */
|
|
67
68
|
onExit: (outcome: FormEditorOutcome) => void;
|
|
68
69
|
/**
|
|
69
70
|
* Pi's `KeybindingsManager` injected as the third arg of the editor
|
|
@@ -75,24 +76,80 @@ export interface InlineFormEditorOpts {
|
|
|
75
76
|
keybindings?: KeybindingsLike;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
80
|
+
|
|
81
|
+
function graphemes(text: string): string[] {
|
|
82
|
+
return Array.from(graphemeSegmenter.segment(text), (s) => s.segment);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function previousGraphemeOffset(text: string, caret: number): number {
|
|
86
|
+
const c = Math.max(0, Math.min(caret, text.length));
|
|
87
|
+
let prev = 0;
|
|
88
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
89
|
+
if (s.index >= c) break;
|
|
90
|
+
prev = s.index;
|
|
91
|
+
}
|
|
92
|
+
return prev;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function nextGraphemeOffset(text: string, caret: number): number {
|
|
96
|
+
const c = Math.max(0, Math.min(caret, text.length));
|
|
97
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
98
|
+
if (s.index >= c) return Math.min(text.length, s.index + s.segment.length);
|
|
99
|
+
if (s.index + s.segment.length > c) return s.index + s.segment.length;
|
|
100
|
+
}
|
|
101
|
+
return text.length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clampGraphemeOffset(text: string, caret: number): number {
|
|
105
|
+
const c = Math.max(0, Math.min(caret, text.length));
|
|
106
|
+
if (c === text.length) return c;
|
|
107
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
108
|
+
if (s.index === c) return c;
|
|
109
|
+
if (s.index > c) break;
|
|
110
|
+
}
|
|
111
|
+
return previousGraphemeOffset(text, c);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function visualColumn(text: string, caret: number): number {
|
|
115
|
+
return visibleWidth(text.slice(0, clampGraphemeOffset(text, caret)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function offsetAtVisualColumn(text: string, targetCol: number): number {
|
|
119
|
+
let col = 0;
|
|
120
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
121
|
+
const w = visibleWidth(s.segment);
|
|
122
|
+
if (col + w > targetCol) return s.index;
|
|
123
|
+
col += w;
|
|
124
|
+
}
|
|
125
|
+
return text.length;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isPrintableGrapheme(data: string): boolean {
|
|
129
|
+
if (data.length === 0 || data.includes("\x1b")) return false;
|
|
130
|
+
for (const ch of data) {
|
|
131
|
+
const code = ch.codePointAt(0);
|
|
132
|
+
if (code === undefined || code < 0x20 || code === 0x7f) return false;
|
|
133
|
+
}
|
|
134
|
+
return graphemes(data).length === 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
78
137
|
/**
|
|
79
138
|
* Move the caret one logical line up inside a multi-line text field.
|
|
80
139
|
* Returns the new caret offset, or `null` when the caret is already on
|
|
81
140
|
* the first logical line — that's the boundary signal the caller uses to
|
|
82
|
-
* fall through to focus-prev. The
|
|
83
|
-
*
|
|
84
|
-
* line's length (same behaviour as Pi's own editor `preferredVisualCol`
|
|
85
|
-
* but at the logical-line level, since the form caret is a single
|
|
86
|
-
* integer offset rather than `{row, col}`).
|
|
141
|
+
* fall through to focus-prev. The visual cell column is preserved across
|
|
142
|
+
* lines, matching pi-tui Editor behaviour for CJK/emoji-width text.
|
|
87
143
|
*/
|
|
88
144
|
function caretLineUp(raw: string, caret: number): number | null {
|
|
89
|
-
const
|
|
145
|
+
const safe = clampGraphemeOffset(raw, caret);
|
|
146
|
+
const lineStart = raw.lastIndexOf("\n", safe - 1) + 1;
|
|
90
147
|
if (lineStart === 0) return null; // first logical line — boundary
|
|
91
148
|
const prevLineEnd = lineStart - 1;
|
|
92
149
|
const prevLineStart = raw.lastIndexOf("\n", prevLineEnd - 1) + 1;
|
|
93
|
-
const colInLine =
|
|
94
|
-
const
|
|
95
|
-
return prevLineStart +
|
|
150
|
+
const colInLine = visualColumn(raw.slice(lineStart, safe), safe - lineStart);
|
|
151
|
+
const prevLine = raw.slice(prevLineStart, prevLineEnd);
|
|
152
|
+
return prevLineStart + offsetAtVisualColumn(prevLine, colInLine);
|
|
96
153
|
}
|
|
97
154
|
|
|
98
155
|
/**
|
|
@@ -101,15 +158,16 @@ function caretLineUp(raw: string, caret: number): number | null {
|
|
|
101
158
|
* the last logical line.
|
|
102
159
|
*/
|
|
103
160
|
function caretLineDown(raw: string, caret: number): number | null {
|
|
104
|
-
const
|
|
161
|
+
const safe = clampGraphemeOffset(raw, caret);
|
|
162
|
+
const nextNl = raw.indexOf("\n", safe);
|
|
105
163
|
if (nextNl === -1) return null; // last logical line — boundary
|
|
106
|
-
const lineStart = raw.lastIndexOf("\n",
|
|
107
|
-
const colInLine =
|
|
164
|
+
const lineStart = raw.lastIndexOf("\n", safe - 1) + 1;
|
|
165
|
+
const colInLine = visualColumn(raw.slice(lineStart, safe), safe - lineStart);
|
|
108
166
|
const nextLineStart = nextNl + 1;
|
|
109
167
|
const nextNlAfter = raw.indexOf("\n", nextLineStart);
|
|
110
168
|
const nextLineEnd = nextNlAfter === -1 ? raw.length : nextNlAfter;
|
|
111
|
-
const
|
|
112
|
-
return nextLineStart +
|
|
169
|
+
const nextLine = raw.slice(nextLineStart, nextLineEnd);
|
|
170
|
+
return nextLineStart + offsetAtVisualColumn(nextLine, colInLine);
|
|
113
171
|
}
|
|
114
172
|
|
|
115
173
|
// ── Bracketed paste handling ─────────────────────────────────────────────
|
|
@@ -307,7 +365,7 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
307
365
|
// Fallback for hosts without bracketed paste: a multi-character
|
|
308
366
|
// chunk of printable text (no escape bytes) is treated as paste.
|
|
309
367
|
// Single-char input still flows through the routeKey path so the
|
|
310
|
-
// existing keystroke handlers (arrows,
|
|
368
|
+
// existing keystroke handlers (arrows, paste, etc.) keep working.
|
|
311
369
|
if (data.length > 1 && isPrintableTextChunk(data)) {
|
|
312
370
|
if (this.applyPaste(data, state)) {
|
|
313
371
|
touch(state);
|
|
@@ -373,23 +431,25 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
373
431
|
private routeKey(data: string, state: InlineFormState): boolean {
|
|
374
432
|
// Globals first. Workflow form contract — these are NOT Pi-configurable
|
|
375
433
|
// editor actions, so they stay as raw byte checks:
|
|
376
|
-
// esc
|
|
377
|
-
// ctrl+
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
434
|
+
// esc (\x1b) — cancel form
|
|
435
|
+
// ctrl+c (\x03) — cancel form
|
|
436
|
+
// ctrl+enter — submit form
|
|
437
|
+
// tab (\t) — focus next field
|
|
438
|
+
// shift+tab (\x1b[Z) — focus previous field
|
|
439
|
+
if (data === "\x03" || matchesKey(data, "escape")) {
|
|
381
440
|
this.opts.onExit("cancel");
|
|
382
441
|
return true;
|
|
383
442
|
}
|
|
384
|
-
if (data
|
|
443
|
+
if (matchesKey(data, "ctrl+enter")) {
|
|
385
444
|
if (this.allValid(state)) this.opts.onExit("submit");
|
|
445
|
+
else this.focusFirstInvalid(state);
|
|
386
446
|
return true;
|
|
387
447
|
}
|
|
388
|
-
if (data
|
|
448
|
+
if (matchesKey(data, "tab")) {
|
|
389
449
|
this.moveFocus(state, +1);
|
|
390
450
|
return true;
|
|
391
451
|
}
|
|
392
|
-
if (data
|
|
452
|
+
if (matchesKey(data, "shift+tab")) {
|
|
393
453
|
this.moveFocus(state, -1);
|
|
394
454
|
return true;
|
|
395
455
|
}
|
|
@@ -520,11 +580,11 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
520
580
|
|
|
521
581
|
// Character cursor movement.
|
|
522
582
|
if (matchesAction(this.kb, data, "tui.editor.cursorLeft")) {
|
|
523
|
-
state.caret =
|
|
583
|
+
state.caret = previousGraphemeOffset(cur, caret);
|
|
524
584
|
return true;
|
|
525
585
|
}
|
|
526
586
|
if (matchesAction(this.kb, data, "tui.editor.cursorRight")) {
|
|
527
|
-
state.caret =
|
|
587
|
+
state.caret = nextGraphemeOffset(cur, caret);
|
|
528
588
|
return true;
|
|
529
589
|
}
|
|
530
590
|
|
|
@@ -562,7 +622,7 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
562
622
|
}
|
|
563
623
|
if (matchesAction(this.kb, data, "tui.editor.deleteCharBackward")) {
|
|
564
624
|
if (caret > 0) {
|
|
565
|
-
const r = deleteRange(cur, caret
|
|
625
|
+
const r = deleteRange(cur, previousGraphemeOffset(cur, caret), caret, caret);
|
|
566
626
|
state.rawText[name] = r.text;
|
|
567
627
|
state.caret = r.caret;
|
|
568
628
|
}
|
|
@@ -570,7 +630,7 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
570
630
|
}
|
|
571
631
|
if (matchesAction(this.kb, data, "tui.editor.deleteCharForward")) {
|
|
572
632
|
if (caret < cur.length) {
|
|
573
|
-
const r = deleteRange(cur, caret, caret
|
|
633
|
+
const r = deleteRange(cur, caret, nextGraphemeOffset(cur, caret), caret);
|
|
574
634
|
state.rawText[name] = r.text;
|
|
575
635
|
state.caret = r.caret;
|
|
576
636
|
}
|
|
@@ -594,12 +654,12 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
594
654
|
return true;
|
|
595
655
|
}
|
|
596
656
|
|
|
597
|
-
// Printable
|
|
657
|
+
// Printable insertion — no Pi action, raw grapheme check. Numeric
|
|
598
658
|
// fields accept the same printable range as text; per-field validation
|
|
599
659
|
// catches non-numeric content at submit time.
|
|
600
|
-
if (data
|
|
660
|
+
if (isPrintableGrapheme(data)) {
|
|
601
661
|
state.rawText[name] = cur.slice(0, caret) + data + cur.slice(caret);
|
|
602
|
-
state.caret = caret +
|
|
662
|
+
state.caret = caret + data.length;
|
|
603
663
|
return true;
|
|
604
664
|
}
|
|
605
665
|
return false;
|
|
@@ -607,11 +667,26 @@ export class InlineFormEditor implements PiEditorComponent {
|
|
|
607
667
|
|
|
608
668
|
private moveFocus(state: InlineFormState, delta: number): void {
|
|
609
669
|
const n = state.fields.length;
|
|
670
|
+
if (n === 0) return;
|
|
610
671
|
state.focusedIdx = (state.focusedIdx + delta + n) % n;
|
|
611
672
|
const next = state.fields[state.focusedIdx]!;
|
|
612
673
|
state.caret = (state.rawText[next.name] ?? "").length;
|
|
613
674
|
}
|
|
614
675
|
|
|
676
|
+
private focusFirstInvalid(state: InlineFormState): void {
|
|
677
|
+
const idx = state.fields.findIndex((f) => {
|
|
678
|
+
const v = state.rawText[f.name] ?? "";
|
|
679
|
+
if (f.required && v.trim() === "") return true;
|
|
680
|
+
if ((f.type === "number" || f.type === "integer") && v !== "" && !Number.isFinite(Number(v))) {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
return f.type === "select" && Boolean(f.choices) && v !== "" && !f.choices!.includes(v);
|
|
684
|
+
});
|
|
685
|
+
if (idx < 0) return;
|
|
686
|
+
state.focusedIdx = idx;
|
|
687
|
+
state.caret = (state.rawText[state.fields[idx]!.name] ?? "").length;
|
|
688
|
+
}
|
|
689
|
+
|
|
615
690
|
private allValid(state: InlineFormState): boolean {
|
|
616
691
|
for (const f of state.fields) {
|
|
617
692
|
const v = state.rawText[f.name] ?? "";
|