@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
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* ╰──────────────────────────────────────────╯
|
|
23
23
|
* select · required · How aggressively to scope the work.
|
|
24
24
|
*
|
|
25
|
-
* tab next · shift+tab prev · ctrl+
|
|
25
|
+
* tab next · shift+tab prev · ctrl+enter run · esc cancel
|
|
26
26
|
*
|
|
27
27
|
* Field-type renderers:
|
|
28
28
|
* - string / number : single-row text input with blinking cursor
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
import type { WorkflowInputEntry } from "../extension/render-result.js";
|
|
41
41
|
import type { GraphTheme } from "./graph-theme.js";
|
|
42
42
|
import { paint } from "./color-utils.js";
|
|
43
|
-
import { truncateToWidth, visibleWidth } from "./text-helpers.js";
|
|
43
|
+
import { matchesKey, truncateToWidth, visibleWidth } from "./text-helpers.js";
|
|
44
44
|
import {
|
|
45
45
|
type KeybindingsLike,
|
|
46
46
|
deleteRange,
|
|
@@ -75,8 +75,8 @@ export interface InputsPickerState {
|
|
|
75
75
|
confirmOpen: boolean;
|
|
76
76
|
/**
|
|
77
77
|
* Set of field indices that failed validation on the most recent submit
|
|
78
|
-
* attempt. Used to dim the
|
|
79
|
-
*
|
|
78
|
+
* attempt. Used to dim the run hint and to highlight a field if the user
|
|
79
|
+
* retries with required fields still empty.
|
|
80
80
|
*/
|
|
81
81
|
invalidIndices: readonly number[];
|
|
82
82
|
/** Cursor offset within the focused single-line text field. */
|
|
@@ -211,8 +211,8 @@ export function coerceValues(
|
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
213
|
* Return the reason why `field` is invalid for `value`, or `null` if valid.
|
|
214
|
-
* Used both to flag fields on submit and to drive the dim state of the
|
|
215
|
-
*
|
|
214
|
+
* Used both to flag fields on submit and to drive the dim state of the run
|
|
215
|
+
* key hint.
|
|
216
216
|
*/
|
|
217
217
|
export function invalidForField(
|
|
218
218
|
field: WorkflowInputEntry,
|
|
@@ -256,6 +256,158 @@ function computeInvalid(
|
|
|
256
256
|
|
|
257
257
|
const dimSep = (theme: GraphTheme): string => paint(" · ", theme.dim);
|
|
258
258
|
|
|
259
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
260
|
+
|
|
261
|
+
function graphemes(text: string): string[] {
|
|
262
|
+
return Array.from(graphemeSegmenter.segment(text), (s) => s.segment);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function previousGraphemeOffset(text: string, caret: number): number {
|
|
266
|
+
const c = Math.max(0, Math.min(caret, text.length));
|
|
267
|
+
let prev = 0;
|
|
268
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
269
|
+
if (s.index >= c) break;
|
|
270
|
+
prev = s.index;
|
|
271
|
+
}
|
|
272
|
+
return prev;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function nextGraphemeOffset(text: string, caret: number): number {
|
|
276
|
+
const c = Math.max(0, Math.min(caret, text.length));
|
|
277
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
278
|
+
if (s.index >= c) return Math.min(text.length, s.index + s.segment.length);
|
|
279
|
+
if (s.index + s.segment.length > c) return s.index + s.segment.length;
|
|
280
|
+
}
|
|
281
|
+
return text.length;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function clampGraphemeOffset(text: string, caret: number): number {
|
|
285
|
+
const c = Math.max(0, Math.min(caret, text.length));
|
|
286
|
+
if (c === text.length) return c;
|
|
287
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
288
|
+
if (s.index === c) return c;
|
|
289
|
+
if (s.index > c) break;
|
|
290
|
+
}
|
|
291
|
+
return previousGraphemeOffset(text, c);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function headToWidth(text: string, width: number): string {
|
|
295
|
+
if (width <= 0) return "";
|
|
296
|
+
let out = "";
|
|
297
|
+
let used = 0;
|
|
298
|
+
for (const g of graphemes(text)) {
|
|
299
|
+
const w = visibleWidth(g);
|
|
300
|
+
if (used + w > width) break;
|
|
301
|
+
out += g;
|
|
302
|
+
used += w;
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function tailToWidth(text: string, width: number): string {
|
|
308
|
+
if (width <= 0) return "";
|
|
309
|
+
let out = "";
|
|
310
|
+
let used = 0;
|
|
311
|
+
const gs = graphemes(text);
|
|
312
|
+
for (let i = gs.length - 1; i >= 0; i--) {
|
|
313
|
+
const g = gs[i]!;
|
|
314
|
+
const w = visibleWidth(g);
|
|
315
|
+
if (used + w > width) break;
|
|
316
|
+
out = g + out;
|
|
317
|
+
used += w;
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function isPrintableGrapheme(data: string): boolean {
|
|
323
|
+
if (data.length === 0 || data.includes("\x1b")) return false;
|
|
324
|
+
for (const ch of data) {
|
|
325
|
+
const code = ch.codePointAt(0);
|
|
326
|
+
if (code === undefined || code < 0x20 || code === 0x7f) return false;
|
|
327
|
+
}
|
|
328
|
+
return graphemes(data).length === 1;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
interface TextLayoutLine {
|
|
332
|
+
text: string;
|
|
333
|
+
start: number;
|
|
334
|
+
end: number;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function layoutEditableText(raw: string, usable: number): TextLayoutLine[] {
|
|
338
|
+
const width = Math.max(1, Math.floor(usable));
|
|
339
|
+
const lines: TextLayoutLine[] = [];
|
|
340
|
+
let line = "";
|
|
341
|
+
let lineStart = 0;
|
|
342
|
+
let lineWidth = 0;
|
|
343
|
+
for (const s of graphemeSegmenter.segment(raw)) {
|
|
344
|
+
const offset = s.index;
|
|
345
|
+
const g = s.segment;
|
|
346
|
+
if (g === "\n") {
|
|
347
|
+
lines.push({ text: line, start: lineStart, end: offset });
|
|
348
|
+
line = "";
|
|
349
|
+
lineStart = offset + g.length;
|
|
350
|
+
lineWidth = 0;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const w = visibleWidth(g);
|
|
354
|
+
if (line !== "" && lineWidth + w > width) {
|
|
355
|
+
lines.push({ text: line, start: lineStart, end: offset });
|
|
356
|
+
line = "";
|
|
357
|
+
lineStart = offset;
|
|
358
|
+
lineWidth = 0;
|
|
359
|
+
}
|
|
360
|
+
line += g;
|
|
361
|
+
lineWidth += w;
|
|
362
|
+
if (lineWidth >= width) {
|
|
363
|
+
lines.push({ text: line, start: lineStart, end: offset + g.length });
|
|
364
|
+
line = "";
|
|
365
|
+
lineStart = offset + g.length;
|
|
366
|
+
lineWidth = 0;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
lines.push({ text: line, start: lineStart, end: raw.length });
|
|
370
|
+
return lines;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function visualColumnAt(text: string, caret: number): number {
|
|
374
|
+
return visibleWidth(text.slice(0, clampGraphemeOffset(text, caret)));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function offsetAtVisualColumn(text: string, targetCol: number): number {
|
|
378
|
+
let col = 0;
|
|
379
|
+
for (const s of graphemeSegmenter.segment(text)) {
|
|
380
|
+
const w = visibleWidth(s.segment);
|
|
381
|
+
if (col + w > targetCol) return s.index;
|
|
382
|
+
col += w;
|
|
383
|
+
}
|
|
384
|
+
return text.length;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function caretLineUp(raw: string, caret: number): number | null {
|
|
388
|
+
const safe = clampGraphemeOffset(raw, caret);
|
|
389
|
+
const lineStartOffset = raw.lastIndexOf("\n", safe - 1) + 1;
|
|
390
|
+
if (lineStartOffset === 0) return null;
|
|
391
|
+
const prevLineEnd = lineStartOffset - 1;
|
|
392
|
+
const prevLineStart = raw.lastIndexOf("\n", prevLineEnd - 1) + 1;
|
|
393
|
+
const col = visualColumnAt(raw.slice(lineStartOffset, safe), raw.slice(lineStartOffset, safe).length);
|
|
394
|
+
const prevLine = raw.slice(prevLineStart, prevLineEnd);
|
|
395
|
+
return prevLineStart + offsetAtVisualColumn(prevLine, col);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function caretLineDown(raw: string, caret: number): number | null {
|
|
399
|
+
const safe = clampGraphemeOffset(raw, caret);
|
|
400
|
+
const nextNl = raw.indexOf("\n", safe);
|
|
401
|
+
if (nextNl === -1) return null;
|
|
402
|
+
const lineStartOffset = raw.lastIndexOf("\n", safe - 1) + 1;
|
|
403
|
+
const col = visualColumnAt(raw.slice(lineStartOffset, safe), raw.slice(lineStartOffset, safe).length);
|
|
404
|
+
const nextLineStart = nextNl + 1;
|
|
405
|
+
const nextNlAfter = raw.indexOf("\n", nextLineStart);
|
|
406
|
+
const nextLineEnd = nextNlAfter === -1 ? raw.length : nextNlAfter;
|
|
407
|
+
const nextLine = raw.slice(nextLineStart, nextLineEnd);
|
|
408
|
+
return nextLineStart + offsetAtVisualColumn(nextLine, col);
|
|
409
|
+
}
|
|
410
|
+
|
|
259
411
|
/**
|
|
260
412
|
* Render a single field's three-row block: top border with title, content
|
|
261
413
|
* row (variable per type), bottom border, then the caption row underneath.
|
|
@@ -377,15 +529,57 @@ function renderFieldContent(
|
|
|
377
529
|
}
|
|
378
530
|
|
|
379
531
|
if (field.type === "text") {
|
|
380
|
-
// 3-row scrolling textarea —
|
|
532
|
+
// 3-row scrolling textarea — wrap by terminal cell width and keep the
|
|
533
|
+
// cursor's visual row in view. Newlines create hard row breaks.
|
|
381
534
|
const ROWS = 3;
|
|
382
|
-
|
|
383
|
-
|
|
535
|
+
if (raw === "") {
|
|
536
|
+
return Array.from({ length: ROWS }, (_, i) =>
|
|
537
|
+
i === 0
|
|
538
|
+
? renderInlineText("", focused, cursorOn, usable, theme, field.placeholder, true)
|
|
539
|
+
: padLine("", usable),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const layout = layoutEditableText(raw, usable);
|
|
543
|
+
const safeCaret = clampGraphemeOffset(raw, caret);
|
|
544
|
+
let cursorRow = layout.length - 1;
|
|
545
|
+
for (let i = 0; i < layout.length; i++) {
|
|
546
|
+
const line = layout[i]!;
|
|
547
|
+
const next = layout[i + 1];
|
|
548
|
+
if (safeCaret >= line.start && safeCaret < line.end) {
|
|
549
|
+
cursorRow = i;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
if (safeCaret === line.end) {
|
|
553
|
+
cursorRow = next?.start === safeCaret ? i + 1 : i;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
cursorRow = Math.max(0, Math.min(cursorRow, layout.length - 1));
|
|
557
|
+
const start = focused
|
|
558
|
+
? Math.max(0, Math.min(cursorRow - ROWS + 1, layout.length - ROWS))
|
|
559
|
+
: Math.max(0, layout.length - ROWS);
|
|
384
560
|
const rows: string[] = [];
|
|
385
561
|
for (let i = 0; i < ROWS; i++) {
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
|
|
562
|
+
const rowIdx = start + i;
|
|
563
|
+
const line = layout[rowIdx];
|
|
564
|
+
if (!line) {
|
|
565
|
+
rows.push(padLine("", usable));
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const lineCaret = safeCaret >= line.start && safeCaret <= line.end
|
|
569
|
+
? safeCaret - line.start
|
|
570
|
+
: line.text.length;
|
|
571
|
+
rows.push(
|
|
572
|
+
renderInlineText(
|
|
573
|
+
line.text,
|
|
574
|
+
focused && rowIdx === cursorRow,
|
|
575
|
+
cursorOn,
|
|
576
|
+
usable,
|
|
577
|
+
theme,
|
|
578
|
+
field.placeholder,
|
|
579
|
+
false,
|
|
580
|
+
lineCaret,
|
|
581
|
+
),
|
|
582
|
+
);
|
|
389
583
|
}
|
|
390
584
|
return rows;
|
|
391
585
|
}
|
|
@@ -415,23 +609,31 @@ function renderInlineText(
|
|
|
415
609
|
if (ph === "") {
|
|
416
610
|
return padLine(showCursor ? paint("▋", theme.accent) : " ", usable);
|
|
417
611
|
}
|
|
418
|
-
const first =
|
|
419
|
-
const rest = ph.slice(1);
|
|
612
|
+
const [first = "", ...rest] = graphemes(ph);
|
|
420
613
|
const head = showCursor
|
|
421
614
|
? paint(first, theme.bg, { bg: theme.accent })
|
|
422
615
|
: paint(first, theme.dim);
|
|
423
|
-
return padLine(head + paint(rest, theme.dim), usable);
|
|
616
|
+
return padLine(head + paint(rest.join(""), theme.dim), usable);
|
|
617
|
+
}
|
|
618
|
+
const safe = clampGraphemeOffset(value, caret ?? value.length);
|
|
619
|
+
const beforeFull = value.slice(0, safe);
|
|
620
|
+
const afterFull = value.slice(safe);
|
|
621
|
+
const [at = ""] = graphemes(afterFull);
|
|
622
|
+
const afterRest = at === "" ? "" : afterFull.slice(at.length);
|
|
623
|
+
const cursorPlain = showCursor ? (at !== "" ? at : "▋") : at;
|
|
624
|
+
const cursorWidth = Math.max(1, visibleWidth(cursorPlain));
|
|
625
|
+
const totalWidth = visibleWidth(beforeFull) + cursorWidth + visibleWidth(showCursor ? afterRest : afterFull.slice(at.length));
|
|
626
|
+
let before = beforeFull;
|
|
627
|
+
let after = showCursor ? afterRest : afterFull.slice(at.length);
|
|
628
|
+
if (totalWidth > usable) {
|
|
629
|
+
before = tailToWidth(beforeFull, Math.max(0, usable - cursorWidth));
|
|
630
|
+
after = headToWidth(showCursor ? afterRest : afterFull.slice(at.length), Math.max(0, usable - visibleWidth(before) - cursorWidth));
|
|
424
631
|
}
|
|
425
|
-
const c = caret ?? value.length;
|
|
426
|
-
const safe = Math.max(0, Math.min(c, value.length));
|
|
427
|
-
const before = value.slice(0, safe);
|
|
428
|
-
const at = value.slice(safe, safe + 1);
|
|
429
|
-
const after = value.slice(safe + 1);
|
|
430
632
|
const cursorCell = showCursor
|
|
431
633
|
? at !== ""
|
|
432
634
|
? paint(at, theme.bg, { bg: theme.accent })
|
|
433
635
|
: paint("▋", theme.accent)
|
|
434
|
-
: at;
|
|
636
|
+
: paint(at, theme.text);
|
|
435
637
|
return padLine(paint(before, theme.text) + cursorCell + paint(after, theme.text), usable);
|
|
436
638
|
}
|
|
437
639
|
|
|
@@ -469,7 +671,8 @@ export function renderInputsPicker(opts: InputsPickerRenderOpts): string[] {
|
|
|
469
671
|
// Section label with field counter (1-based). When the terminal is too
|
|
470
672
|
// narrow to hold both, the counter is the priority — drop "INPUTS" first
|
|
471
673
|
// so the user always knows which field they're on.
|
|
472
|
-
const
|
|
674
|
+
const focusTargetCount = fields.length;
|
|
675
|
+
const counter = `${Math.min(state.focusedIdx + 1, focusTargetCount)} / ${focusTargetCount}`;
|
|
473
676
|
const labelLeft =
|
|
474
677
|
paint("▎ ", theme.mauve) + paint("INPUTS", theme.textMuted, { bold: true });
|
|
475
678
|
const labelLen = visibleWidth(labelLeft);
|
|
@@ -498,14 +701,12 @@ export function renderInputsPicker(opts: InputsPickerRenderOpts): string[] {
|
|
|
498
701
|
lines.push(""); // gap between fields
|
|
499
702
|
}
|
|
500
703
|
|
|
501
|
-
// Footer hints — tiered for narrow widths. The widest form ends up around
|
|
502
|
-
// 57 visible cells; we step down to keys-with-labels-tight, keys-only,
|
|
503
|
-
// and finally essentials-only when the terminal cannot hold the row. The
|
|
504
|
-
// `ctrl+s` hint dims when any field is currently invalid.
|
|
505
704
|
const anyInvalid = computeInvalid(fields, state.rawText).length > 0;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
705
|
+
|
|
706
|
+
// Footer hints — tiered for narrow widths. The widest form ends up around
|
|
707
|
+
// 61 visible cells; we step down to keys-with-labels-tight, keys-only,
|
|
708
|
+
// and finally essentials-only when the terminal cannot hold the row.
|
|
709
|
+
lines.push(renderFooterHints(width, theme, anyInvalid));
|
|
509
710
|
|
|
510
711
|
if (state.confirmOpen) {
|
|
511
712
|
lines.push("");
|
|
@@ -517,47 +718,41 @@ export function renderInputsPicker(opts: InputsPickerRenderOpts): string[] {
|
|
|
517
718
|
/**
|
|
518
719
|
* Footer hint row, tier-degraded so it never wraps on resize. Tiers:
|
|
519
720
|
*
|
|
520
|
-
* wide (≥ widest): tab next · shift+tab prev · ctrl+
|
|
521
|
-
* medium (≥ keys): tab · shift+tab · ctrl+
|
|
522
|
-
* tight (≥ short): tab · ⇧tab ·
|
|
523
|
-
* narrow (else):
|
|
524
|
-
*
|
|
525
|
-
* The `ctrl+s` hint always survives — it is the only "run" affordance — and
|
|
526
|
-
* `esc cancel` always survives so the user can back out.
|
|
721
|
+
* wide (≥ widest): tab next · shift+tab prev · ctrl+enter run · esc cancel
|
|
722
|
+
* medium (≥ keys): tab · shift+tab · ctrl+enter · esc
|
|
723
|
+
* tight (≥ short): tab · ⇧tab · ⌃↵ · esc
|
|
724
|
+
* narrow (else): ⌃↵ · esc
|
|
527
725
|
*/
|
|
528
|
-
function renderFooterHints(
|
|
529
|
-
width: number,
|
|
530
|
-
theme: GraphTheme,
|
|
531
|
-
submitColor: string,
|
|
532
|
-
submitLabelColor: string,
|
|
533
|
-
): string {
|
|
726
|
+
function renderFooterHints(width: number, theme: GraphTheme, submitDisabled: boolean): string {
|
|
534
727
|
const sep = dimSep(theme);
|
|
535
728
|
const sepWidth = 5; // " · "
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
const
|
|
729
|
+
const submitColor = submitDisabled ? theme.dim : theme.text;
|
|
730
|
+
const submitLabelColor = submitDisabled ? theme.dim : theme.textMuted;
|
|
731
|
+
const hint = (key: string, label: string, keyColor = theme.text, labelColor = theme.textMuted): string =>
|
|
732
|
+
paint(key, keyColor) + " " + paint(label, labelColor);
|
|
733
|
+
const keyOnly = (key: string, keyColor = theme.text): string => paint(key, keyColor);
|
|
539
734
|
|
|
540
735
|
const wide = [
|
|
541
|
-
{ width: 8, render: () => hint("tab", "
|
|
542
|
-
{ width: 14, render: () => hint("shift+tab", "
|
|
543
|
-
{ width:
|
|
544
|
-
{ width: 10, render: () => hint("esc", "
|
|
736
|
+
{ width: 8, render: () => hint("tab", "Next") },
|
|
737
|
+
{ width: 14, render: () => hint("shift+tab", "Prev") },
|
|
738
|
+
{ width: 14, render: () => hint("ctrl+enter", "Run", submitColor, submitLabelColor) },
|
|
739
|
+
{ width: 10, render: () => hint("esc", "Cancel") },
|
|
545
740
|
];
|
|
546
741
|
const medium = [
|
|
547
|
-
{ width: 3, render: () => keyOnly("tab"
|
|
548
|
-
{ width: 9, render: () => keyOnly("shift+tab"
|
|
549
|
-
{ width:
|
|
550
|
-
{ width:
|
|
742
|
+
{ width: 3, render: () => keyOnly("tab") },
|
|
743
|
+
{ width: 9, render: () => keyOnly("shift+tab") },
|
|
744
|
+
{ width: 10, render: () => keyOnly("ctrl+enter", submitColor) },
|
|
745
|
+
{ width: 6, render: () => keyOnly("esc") },
|
|
551
746
|
];
|
|
552
747
|
const tight = [
|
|
553
|
-
{ width: 3, render: () => keyOnly("tab"
|
|
554
|
-
{ width: 4, render: () => keyOnly("⇧tab"
|
|
555
|
-
{ width: 2, render: () => keyOnly("
|
|
556
|
-
{ width:
|
|
748
|
+
{ width: 3, render: () => keyOnly("tab") },
|
|
749
|
+
{ width: 4, render: () => keyOnly("⇧tab") },
|
|
750
|
+
{ width: 2, render: () => keyOnly("⌃↵", submitColor) },
|
|
751
|
+
{ width: 6, render: () => keyOnly("esc") },
|
|
557
752
|
];
|
|
558
753
|
const narrow = [
|
|
559
|
-
{ width: 2, render: () => keyOnly("
|
|
560
|
-
{ width:
|
|
754
|
+
{ width: 2, render: () => keyOnly("⌃↵", submitColor) },
|
|
755
|
+
{ width: 6, render: () => keyOnly("esc") },
|
|
561
756
|
];
|
|
562
757
|
|
|
563
758
|
for (const tier of [wide, medium, tight, narrow]) {
|
|
@@ -567,7 +762,7 @@ function renderFooterHints(
|
|
|
567
762
|
}
|
|
568
763
|
}
|
|
569
764
|
// Truly tiny terminal — show just the run+cancel keys joined by a single space.
|
|
570
|
-
return paint("
|
|
765
|
+
return paint("⌃↵", submitColor) + " " + paint("esc", theme.text);
|
|
571
766
|
}
|
|
572
767
|
|
|
573
768
|
/**
|
|
@@ -632,9 +827,9 @@ function shortVal(s: string): string {
|
|
|
632
827
|
* left / right — select: cycle choices; boolean: flip; text: caret
|
|
633
828
|
* space — boolean: flip
|
|
634
829
|
* enter — text: newline; otherwise: next field
|
|
635
|
-
* ctrl+
|
|
830
|
+
* ctrl+enter — open confirm modal (if all required filled)
|
|
636
831
|
* backspace — delete char left of caret
|
|
637
|
-
* esc
|
|
832
|
+
* esc / ctrl+c — close picker without running
|
|
638
833
|
*
|
|
639
834
|
* Keys (confirm modal mode):
|
|
640
835
|
* y / enter — run
|
|
@@ -650,7 +845,7 @@ export function handleInputsPickerInput(
|
|
|
650
845
|
// Defensive: a workflow with zero declared inputs shouldn't reach the
|
|
651
846
|
// picker (we gate on `fields.length > 0` at the open() site), but if
|
|
652
847
|
// it does, treat any keystroke as a noop and let the host close us.
|
|
653
|
-
if (key
|
|
848
|
+
if (isCancelKey(key)) return { kind: "cancel" };
|
|
654
849
|
return { kind: "noop" };
|
|
655
850
|
}
|
|
656
851
|
if (state.confirmOpen) return handleConfirmKey(key, state, fields);
|
|
@@ -663,33 +858,21 @@ function handleFormKey(
|
|
|
663
858
|
fields: readonly WorkflowInputEntry[],
|
|
664
859
|
kb: KeybindingsLike | undefined,
|
|
665
860
|
): InputsPickerAction {
|
|
666
|
-
const field = fields[state.focusedIdx]!;
|
|
667
|
-
const name = field.name;
|
|
668
|
-
const cur = state.rawText[name] ?? "";
|
|
669
|
-
|
|
670
861
|
// ── Global navigation (workflow form contract, not Pi actions) ──
|
|
671
|
-
if (key
|
|
672
|
-
if (key
|
|
862
|
+
if (isCancelKey(key)) return { kind: "cancel" };
|
|
863
|
+
if (matchesKey(key, "ctrl+enter")) return attemptPickerSubmit(state, fields);
|
|
864
|
+
if (matchesKey(key, "tab")) {
|
|
673
865
|
moveFocus(state, fields, +1);
|
|
674
866
|
return { kind: "noop" };
|
|
675
867
|
}
|
|
676
|
-
if (key
|
|
868
|
+
if (matchesKey(key, "shift+tab")) {
|
|
677
869
|
moveFocus(state, fields, -1);
|
|
678
870
|
return { kind: "noop" };
|
|
679
871
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
state.invalidIndices = invalid;
|
|
685
|
-
state.focusedIdx = invalid[0]!;
|
|
686
|
-
state.caret = (state.rawText[fields[state.focusedIdx]!.name] ?? "").length;
|
|
687
|
-
return { kind: "noop" };
|
|
688
|
-
}
|
|
689
|
-
state.invalidIndices = [];
|
|
690
|
-
state.confirmOpen = true;
|
|
691
|
-
return { kind: "noop" };
|
|
692
|
-
}
|
|
872
|
+
|
|
873
|
+
const field = fields[state.focusedIdx]!;
|
|
874
|
+
const name = field.name;
|
|
875
|
+
const cur = state.rawText[name] ?? "";
|
|
693
876
|
|
|
694
877
|
// ── Per-type edits ──
|
|
695
878
|
if (field.type === "select") {
|
|
@@ -705,10 +888,24 @@ function handleFormKey(
|
|
|
705
888
|
const caret = Math.max(0, Math.min(state.caret, cur.length));
|
|
706
889
|
|
|
707
890
|
if (matchesAction(kb, key, "tui.editor.cursorUp")) {
|
|
891
|
+
if (field.type === "text") {
|
|
892
|
+
const nextCaret = caretLineUp(cur, caret);
|
|
893
|
+
if (nextCaret !== null) {
|
|
894
|
+
state.caret = nextCaret;
|
|
895
|
+
return { kind: "noop" };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
708
898
|
moveFocus(state, fields, -1);
|
|
709
899
|
return { kind: "noop" };
|
|
710
900
|
}
|
|
711
901
|
if (matchesAction(kb, key, "tui.editor.cursorDown")) {
|
|
902
|
+
if (field.type === "text") {
|
|
903
|
+
const nextCaret = caretLineDown(cur, caret);
|
|
904
|
+
if (nextCaret !== null) {
|
|
905
|
+
state.caret = nextCaret;
|
|
906
|
+
return { kind: "noop" };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
712
909
|
moveFocus(state, fields, +1);
|
|
713
910
|
return { kind: "noop" };
|
|
714
911
|
}
|
|
@@ -729,11 +926,11 @@ function handleFormKey(
|
|
|
729
926
|
return { kind: "noop" };
|
|
730
927
|
}
|
|
731
928
|
if (matchesAction(kb, key, "tui.editor.cursorLeft")) {
|
|
732
|
-
state.caret =
|
|
929
|
+
state.caret = previousGraphemeOffset(cur, caret);
|
|
733
930
|
return { kind: "noop" };
|
|
734
931
|
}
|
|
735
932
|
if (matchesAction(kb, key, "tui.editor.cursorRight")) {
|
|
736
|
-
state.caret =
|
|
933
|
+
state.caret = nextGraphemeOffset(cur, caret);
|
|
737
934
|
return { kind: "noop" };
|
|
738
935
|
}
|
|
739
936
|
if (matchesAction(kb, key, "tui.editor.deleteWordBackward")) {
|
|
@@ -766,7 +963,7 @@ function handleFormKey(
|
|
|
766
963
|
}
|
|
767
964
|
if (matchesAction(kb, key, "tui.editor.deleteCharBackward")) {
|
|
768
965
|
if (caret > 0) {
|
|
769
|
-
const r = deleteRange(cur, caret
|
|
966
|
+
const r = deleteRange(cur, previousGraphemeOffset(cur, caret), caret, caret);
|
|
770
967
|
state.rawText[name] = r.text;
|
|
771
968
|
state.caret = r.caret;
|
|
772
969
|
}
|
|
@@ -774,7 +971,7 @@ function handleFormKey(
|
|
|
774
971
|
}
|
|
775
972
|
if (matchesAction(kb, key, "tui.editor.deleteCharForward")) {
|
|
776
973
|
if (caret < cur.length) {
|
|
777
|
-
const r = deleteRange(cur, caret, caret
|
|
974
|
+
const r = deleteRange(cur, caret, nextGraphemeOffset(cur, caret), caret);
|
|
778
975
|
state.rawText[name] = r.text;
|
|
779
976
|
state.caret = r.caret;
|
|
780
977
|
}
|
|
@@ -792,10 +989,11 @@ function handleFormKey(
|
|
|
792
989
|
}
|
|
793
990
|
return { kind: "noop" };
|
|
794
991
|
}
|
|
795
|
-
// Printable insert.
|
|
796
|
-
|
|
992
|
+
// Printable insert. Accept exactly one grapheme cluster so CJK, emoji ZWJ
|
|
993
|
+
// sequences, and combining-mark input follow pi-tui Input semantics.
|
|
994
|
+
if (isPrintableGrapheme(key)) {
|
|
797
995
|
state.rawText[name] = cur.slice(0, caret) + key + cur.slice(caret);
|
|
798
|
-
state.caret = caret +
|
|
996
|
+
state.caret = caret + key.length;
|
|
799
997
|
return { kind: "noop" };
|
|
800
998
|
}
|
|
801
999
|
return { kind: "noop" };
|
|
@@ -869,19 +1067,41 @@ function handleConfirmKey(
|
|
|
869
1067
|
if (key === "y" || key === "Y" || key === "\r" || key === "\n") {
|
|
870
1068
|
return { kind: "run", values: coerceValues(fields, state.rawText) };
|
|
871
1069
|
}
|
|
872
|
-
if (key === "
|
|
1070
|
+
if (key === "\x03") return { kind: "cancel" };
|
|
1071
|
+
if (key === "n" || key === "N" || matchesKey(key, "escape")) {
|
|
873
1072
|
state.confirmOpen = false;
|
|
874
1073
|
return { kind: "noop" };
|
|
875
1074
|
}
|
|
876
1075
|
return { kind: "noop" };
|
|
877
1076
|
}
|
|
878
1077
|
|
|
1078
|
+
function isCancelKey(key: string): boolean {
|
|
1079
|
+
return key === "\x03" || matchesKey(key, "escape");
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function attemptPickerSubmit(
|
|
1083
|
+
state: InputsPickerState,
|
|
1084
|
+
fields: readonly WorkflowInputEntry[],
|
|
1085
|
+
): InputsPickerAction {
|
|
1086
|
+
const invalid = computeInvalid(fields, state.rawText);
|
|
1087
|
+
if (invalid.length > 0) {
|
|
1088
|
+
state.invalidIndices = invalid;
|
|
1089
|
+
state.focusedIdx = invalid[0]!;
|
|
1090
|
+
state.caret = (state.rawText[fields[state.focusedIdx]!.name] ?? "").length;
|
|
1091
|
+
return { kind: "noop" };
|
|
1092
|
+
}
|
|
1093
|
+
state.invalidIndices = [];
|
|
1094
|
+
state.confirmOpen = true;
|
|
1095
|
+
return { kind: "noop" };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
879
1098
|
function moveFocus(
|
|
880
1099
|
state: InputsPickerState,
|
|
881
1100
|
fields: readonly WorkflowInputEntry[],
|
|
882
1101
|
delta: number,
|
|
883
1102
|
): void {
|
|
884
1103
|
const n = fields.length;
|
|
1104
|
+
if (n === 0) return;
|
|
885
1105
|
state.focusedIdx = (state.focusedIdx + delta + n) % n;
|
|
886
1106
|
const next = fields[state.focusedIdx]!;
|
|
887
1107
|
state.caret = (state.rawText[next.name] ?? "").length;
|