@bastani/atomic 0.5.4 → 0.5.5-0
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/README.md +44 -1
- package/dist/lib/path-root-guard.d.ts +4 -0
- package/dist/lib/path-root-guard.d.ts.map +1 -0
- package/dist/sdk/components/color-utils.d.ts +1 -0
- package/dist/sdk/components/color-utils.d.ts.map +1 -0
- package/dist/sdk/components/connectors.d.ts +3 -2
- package/dist/sdk/components/connectors.d.ts.map +1 -0
- package/dist/sdk/components/connectors.test.d.ts +1 -0
- package/dist/sdk/components/connectors.test.d.ts.map +1 -0
- package/dist/sdk/components/edge.d.ts +2 -1
- package/dist/sdk/components/edge.d.ts.map +1 -0
- package/dist/sdk/components/error-boundary.d.ts +1 -0
- package/dist/sdk/components/error-boundary.d.ts.map +1 -0
- package/dist/sdk/components/graph-theme.d.ts +2 -1
- package/dist/sdk/components/graph-theme.d.ts.map +1 -0
- package/dist/sdk/components/header.d.ts +1 -0
- package/dist/sdk/components/header.d.ts.map +1 -0
- package/dist/sdk/components/hooks.d.ts +15 -0
- package/dist/sdk/components/hooks.d.ts.map +1 -0
- package/dist/sdk/components/layout.d.ts +2 -1
- package/dist/sdk/components/layout.d.ts.map +1 -0
- package/dist/sdk/components/layout.test.d.ts +1 -0
- package/dist/sdk/components/layout.test.d.ts.map +1 -0
- package/dist/sdk/components/node-card.d.ts +5 -3
- package/dist/sdk/components/node-card.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-contexts.d.ts +3 -2
- package/dist/sdk/components/orchestrator-panel-contexts.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-store.d.ts +2 -1
- package/dist/sdk/components/orchestrator-panel-store.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-store.test.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-store.test.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts +1 -0
- package/dist/sdk/components/orchestrator-panel-types.d.ts.map +1 -0
- package/dist/sdk/components/orchestrator-panel.d.ts +2 -1
- package/dist/sdk/components/orchestrator-panel.d.ts.map +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts +1 -0
- package/dist/sdk/components/session-graph-panel.d.ts.map +1 -0
- package/dist/sdk/components/status-helpers.d.ts +2 -1
- package/dist/sdk/components/status-helpers.d.ts.map +1 -0
- package/dist/sdk/components/statusline.d.ts +2 -1
- package/dist/sdk/components/statusline.d.ts.map +1 -0
- package/dist/sdk/components/workflow-picker-panel.d.ts +11 -8
- package/dist/sdk/components/workflow-picker-panel.d.ts.map +1 -0
- package/dist/sdk/define-workflow.d.ts +2 -1
- package/dist/sdk/define-workflow.d.ts.map +1 -0
- package/dist/sdk/define-workflow.test.d.ts +1 -0
- package/dist/sdk/define-workflow.test.d.ts.map +1 -0
- package/dist/sdk/errors.d.ts +3 -0
- package/dist/sdk/errors.d.ts.map +1 -0
- package/dist/sdk/errors.test.d.ts +2 -0
- package/dist/sdk/errors.test.d.ts.map +1 -0
- package/dist/sdk/index.d.ts +7 -6
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/providers/claude.d.ts +17 -6
- package/dist/sdk/providers/claude.d.ts.map +1 -0
- package/dist/sdk/providers/copilot.d.ts +2 -5
- package/dist/sdk/providers/copilot.d.ts.map +1 -0
- package/dist/sdk/providers/opencode.d.ts +2 -5
- package/dist/sdk/providers/opencode.d.ts.map +1 -0
- package/dist/sdk/runtime/discovery.d.ts +2 -1
- package/dist/sdk/runtime/discovery.d.ts.map +1 -0
- package/dist/sdk/runtime/executor-entry.d.ts +1 -0
- package/dist/sdk/runtime/executor-entry.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.d.ts +3 -6
- package/dist/sdk/runtime/executor.d.ts.map +1 -0
- package/dist/sdk/runtime/executor.test.d.ts +1 -0
- package/dist/sdk/runtime/executor.test.d.ts.map +1 -0
- package/dist/sdk/runtime/graph-inference.d.ts +1 -0
- package/dist/sdk/runtime/graph-inference.d.ts.map +1 -0
- package/dist/sdk/runtime/loader.d.ts +5 -7
- package/dist/sdk/runtime/loader.d.ts.map +1 -0
- package/dist/sdk/runtime/panel.d.ts +3 -2
- package/dist/sdk/runtime/panel.d.ts.map +1 -0
- package/dist/sdk/runtime/theme.d.ts +1 -0
- package/dist/sdk/runtime/theme.d.ts.map +1 -0
- package/dist/sdk/runtime/tmux.d.ts +26 -8
- package/dist/sdk/runtime/tmux.d.ts.map +1 -0
- package/dist/sdk/types.d.ts +23 -1
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/claude/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/copilot/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/heuristic.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts +2 -1
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/helpers/scout.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/git.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts +2 -1
- package/dist/sdk/workflows/builtin/ralph/helpers/review.d.ts.map +1 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts +1 -0
- package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -0
- package/dist/sdk/workflows/index.d.ts +14 -14
- package/dist/sdk/workflows/index.d.ts.map +1 -0
- package/dist/services/config/definitions.d.ts +85 -0
- package/dist/services/config/definitions.d.ts.map +1 -0
- package/dist/services/system/copy.d.ts +77 -0
- package/dist/services/system/copy.d.ts.map +1 -0
- package/dist/services/system/detect.d.ts +75 -0
- package/dist/services/system/detect.d.ts.map +1 -0
- package/package.json +13 -34
- package/src/cli.ts +11 -10
- package/src/commands/cli/chat/index.ts +11 -11
- package/src/commands/cli/chat.ts +1 -1
- package/src/commands/cli/config.ts +10 -9
- package/src/commands/cli/init/index.ts +11 -11
- package/src/commands/cli/init/onboarding.ts +4 -4
- package/src/commands/cli/init/scm.ts +5 -5
- package/src/commands/cli/init.ts +1 -1
- package/src/commands/cli/workflow-command.test.ts +19 -11
- package/src/commands/cli/workflow.test.ts +2 -2
- package/src/commands/cli/workflow.ts +6 -6
- package/src/lib/merge.ts +17 -31
- package/src/lib/path-root-guard.ts +2 -2
- package/src/lib/spawn.ts +13 -7
- package/src/scripts/bump-version.ts +1 -1
- package/src/scripts/constants.ts +2 -2
- package/src/sdk/components/header.tsx +21 -23
- package/src/sdk/components/hooks.ts +21 -0
- package/src/sdk/components/node-card.tsx +3 -2
- package/src/sdk/components/session-graph-panel.tsx +14 -18
- package/src/sdk/components/workflow-picker-panel.tsx +201 -216
- package/src/sdk/errors.test.ts +56 -0
- package/src/sdk/errors.ts +5 -0
- package/src/sdk/providers/claude.ts +279 -70
- package/src/sdk/providers/copilot.ts +17 -27
- package/src/sdk/providers/opencode.ts +17 -27
- package/src/sdk/runtime/discovery.ts +18 -18
- package/src/sdk/runtime/executor.test.ts +15 -48
- package/src/sdk/runtime/executor.ts +152 -121
- package/src/sdk/runtime/loader.ts +16 -21
- package/src/sdk/runtime/tmux.ts +95 -32
- package/src/sdk/types.ts +45 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +27 -0
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/prompts.ts +25 -16
- package/src/sdk/workflows/builtin/deep-research-codebase/helpers/scout.ts +25 -24
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +5 -0
- package/src/sdk/workflows/index.ts +3 -3
- package/src/services/config/atomic-config.ts +7 -8
- package/src/services/config/atomic-global-config.ts +9 -9
- package/src/services/config/config-path.ts +1 -1
- package/src/services/config/definitions.ts +3 -4
- package/src/services/config/index.ts +1 -1
- package/src/services/config/settings.ts +30 -36
- package/src/services/system/agents.ts +3 -3
- package/src/services/system/auto-sync.ts +9 -9
- package/src/services/system/copy.ts +9 -9
- package/src/services/system/file-lock.ts +2 -2
- package/src/services/system/install-ui.ts +2 -2
- package/src/services/system/skills.ts +1 -1
- package/src/theme/colors.ts +1 -1
- package/src/theme/logo.ts +1 -1
- package/tsconfig.json +3 -4
- package/dist/chunk-1gb5qxz9.js +0 -1
- package/dist/chunk-fdk7tact.js +0 -417
- package/dist/chunk-xkxndz5g.js +0 -1041
- package/dist/sdk/index.js +0 -52
- package/dist/sdk/workflows/builtin/ralph/claude/index.js +0 -96
- package/dist/sdk/workflows/builtin/ralph/copilot/index.js +0 -119
- package/dist/sdk/workflows/builtin/ralph/opencode/index.js +0 -148
- package/dist/sdk/workflows/index.js +0 -100
- package/src/commands/cli/chat/client.ts +0 -18
|
@@ -26,13 +26,18 @@
|
|
|
26
26
|
* `{ workflow, inputs }` record if they confirm the run.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createCliRenderer,
|
|
31
|
+
type CliRenderer,
|
|
32
|
+
type TextareaRenderable,
|
|
33
|
+
} from "@opentui/core";
|
|
30
34
|
import {
|
|
31
35
|
createRoot,
|
|
32
36
|
useKeyboard,
|
|
33
37
|
type Root,
|
|
34
38
|
} from "@opentui/react";
|
|
35
|
-
import { useState, useEffect, useMemo } from "react";
|
|
39
|
+
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
|
40
|
+
import { useLatest } from "./hooks.ts";
|
|
36
41
|
import { resolveTheme, type TerminalTheme } from "../runtime/theme.ts";
|
|
37
42
|
import type { AgentType, WorkflowInput } from "../types.ts";
|
|
38
43
|
import type { WorkflowWithMetadata } from "../runtime/discovery.ts";
|
|
@@ -166,11 +171,9 @@ interface ListEntry {
|
|
|
166
171
|
section: Source;
|
|
167
172
|
}
|
|
168
173
|
|
|
169
|
-
|
|
170
|
-
kind: "section"
|
|
171
|
-
|
|
172
|
-
entry?: ListEntry;
|
|
173
|
-
}
|
|
174
|
+
type ListRow =
|
|
175
|
+
| { kind: "section"; source: Source }
|
|
176
|
+
| { kind: "entry"; entry: ListEntry };
|
|
174
177
|
|
|
175
178
|
export function buildEntries(
|
|
176
179
|
query: string,
|
|
@@ -259,13 +262,17 @@ function SectionLabel({
|
|
|
259
262
|
function FilterBar({
|
|
260
263
|
theme,
|
|
261
264
|
query,
|
|
265
|
+
focused,
|
|
266
|
+
onInput,
|
|
262
267
|
}: {
|
|
263
268
|
theme: PickerTheme;
|
|
264
269
|
query: string;
|
|
270
|
+
focused: boolean;
|
|
271
|
+
onInput: (value: string) => void;
|
|
265
272
|
}) {
|
|
266
273
|
return (
|
|
267
274
|
<box
|
|
268
|
-
|
|
275
|
+
minHeight={3}
|
|
269
276
|
border
|
|
270
277
|
borderStyle="rounded"
|
|
271
278
|
borderColor={theme.borderActive}
|
|
@@ -280,14 +287,16 @@ function FilterBar({
|
|
|
280
287
|
<strong>❯ </strong>
|
|
281
288
|
</span>
|
|
282
289
|
</text>
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
{
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
290
|
+
<input
|
|
291
|
+
value={query}
|
|
292
|
+
focused={focused}
|
|
293
|
+
onInput={onInput}
|
|
294
|
+
textColor={theme.text}
|
|
295
|
+
backgroundColor={theme.backgroundPanel}
|
|
296
|
+
focusedBackgroundColor={theme.backgroundPanel}
|
|
297
|
+
focusedTextColor={theme.text}
|
|
298
|
+
flexGrow={1}
|
|
299
|
+
/>
|
|
291
300
|
</box>
|
|
292
301
|
);
|
|
293
302
|
}
|
|
@@ -311,12 +320,23 @@ function WorkflowList({
|
|
|
311
320
|
);
|
|
312
321
|
}
|
|
313
322
|
|
|
314
|
-
|
|
323
|
+
// Pre-compute entry indices so the render pass is side-effect-free.
|
|
324
|
+
const entryIndexByRow = useMemo(() => {
|
|
325
|
+
const map = new Map<number, number>();
|
|
326
|
+
let counter = 0;
|
|
327
|
+
for (let i = 0; i < rows.length; i++) {
|
|
328
|
+
if (rows[i]!.kind === "entry") {
|
|
329
|
+
map.set(i, counter++);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return map;
|
|
333
|
+
}, [rows]);
|
|
334
|
+
|
|
315
335
|
return (
|
|
316
336
|
<box flexDirection="column">
|
|
317
337
|
{rows.map((row, i) => {
|
|
318
338
|
if (row.kind === "section") {
|
|
319
|
-
const src = row.source
|
|
339
|
+
const src = row.source;
|
|
320
340
|
return (
|
|
321
341
|
<box
|
|
322
342
|
key={`s${i}`}
|
|
@@ -335,9 +355,9 @@ function WorkflowList({
|
|
|
335
355
|
</box>
|
|
336
356
|
);
|
|
337
357
|
}
|
|
338
|
-
|
|
339
|
-
const isFocused =
|
|
340
|
-
const wf = row.entry
|
|
358
|
+
const entryIdx = entryIndexByRow.get(i) ?? -1;
|
|
359
|
+
const isFocused = entryIdx === focusedEntryIdx;
|
|
360
|
+
const wf = row.entry.workflow;
|
|
341
361
|
|
|
342
362
|
return (
|
|
343
363
|
<box
|
|
@@ -437,11 +457,11 @@ function Preview({
|
|
|
437
457
|
<box height={1} />
|
|
438
458
|
|
|
439
459
|
<text>
|
|
440
|
-
<span fg={theme[SOURCE_COLOR[wf.source
|
|
441
|
-
{SOURCE_DISPLAY[wf.source
|
|
460
|
+
<span fg={theme[SOURCE_COLOR[wf.source]]}>
|
|
461
|
+
{SOURCE_DISPLAY[wf.source]}
|
|
442
462
|
</span>
|
|
443
463
|
<span fg={theme.textDim}>
|
|
444
|
-
{" (" + SOURCE_DIR[wf.source
|
|
464
|
+
{" (" + SOURCE_DIR[wf.source] + ")"}
|
|
445
465
|
</span>
|
|
446
466
|
</text>
|
|
447
467
|
|
|
@@ -508,97 +528,55 @@ function EmptyPreview({
|
|
|
508
528
|
|
|
509
529
|
const TEXT_FIELD_LINES = 3;
|
|
510
530
|
|
|
511
|
-
/**
|
|
512
|
-
* Render a placeholder with a solid full-cell caret overlapping its
|
|
513
|
-
* first character. The caret is a full cell wide — same thickness as
|
|
514
|
-
* the trailing-caret cell used for typed text — so switching between
|
|
515
|
-
* "empty" and "has input" states never visually halves the cursor.
|
|
516
|
-
*
|
|
517
|
-
* When the field is not focused, the placeholder renders plain dim
|
|
518
|
-
* text without the caret highlight.
|
|
519
|
-
*/
|
|
520
|
-
function PlaceholderWithCursor({
|
|
521
|
-
theme,
|
|
522
|
-
placeholder,
|
|
523
|
-
focused,
|
|
524
|
-
}: {
|
|
525
|
-
theme: PickerTheme;
|
|
526
|
-
placeholder: string;
|
|
527
|
-
focused: boolean;
|
|
528
|
-
}) {
|
|
529
|
-
const effective = placeholder.length > 0 ? placeholder : " ";
|
|
530
|
-
const first = effective.slice(0, 1);
|
|
531
|
-
const rest = effective.slice(1);
|
|
532
|
-
|
|
533
|
-
if (!focused) {
|
|
534
|
-
return (
|
|
535
|
-
<text>
|
|
536
|
-
<span fg={theme.textDim}>{effective}</span>
|
|
537
|
-
</text>
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return (
|
|
542
|
-
<text>
|
|
543
|
-
<span fg={theme.surface} bg={theme.primary}>
|
|
544
|
-
{first}
|
|
545
|
-
</span>
|
|
546
|
-
<span fg={theme.textDim}>{rest}</span>
|
|
547
|
-
</text>
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
531
|
|
|
551
532
|
function TextAreaContent({
|
|
552
533
|
theme,
|
|
553
534
|
value,
|
|
554
535
|
placeholder,
|
|
555
536
|
focused,
|
|
556
|
-
|
|
537
|
+
onChangeRef,
|
|
557
538
|
}: {
|
|
558
539
|
theme: PickerTheme;
|
|
559
540
|
value: string;
|
|
560
541
|
placeholder: string;
|
|
561
542
|
focused: boolean;
|
|
562
|
-
|
|
543
|
+
onChangeRef: React.RefObject<((value: string) => void) | null>;
|
|
563
544
|
}) {
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
545
|
+
const ref = useRef<TextareaRenderable>(null);
|
|
546
|
+
|
|
547
|
+
// Sync external value → textarea when it diverges (e.g. initial value).
|
|
548
|
+
useEffect(() => {
|
|
549
|
+
if (ref.current && ref.current.plainText !== value) {
|
|
550
|
+
ref.current.setText(value);
|
|
551
|
+
}
|
|
552
|
+
}, [value]);
|
|
553
|
+
|
|
554
|
+
// Report changes back to parent via onContentChange.
|
|
555
|
+
useEffect(() => {
|
|
556
|
+
const ta = ref.current;
|
|
557
|
+
if (!ta) return;
|
|
558
|
+
ta.onContentChange = () => {
|
|
559
|
+
onChangeRef.current?.(ta.plainText);
|
|
560
|
+
};
|
|
561
|
+
return () => {
|
|
562
|
+
ta.onContentChange = undefined;
|
|
563
|
+
};
|
|
564
|
+
}, [onChangeRef]);
|
|
572
565
|
|
|
573
566
|
return (
|
|
574
|
-
<
|
|
575
|
-
{
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
// Trailing caret on the active line. Rendered as a
|
|
588
|
-
// background-coloured space so the cell width matches the
|
|
589
|
-
// placeholder-overlap caret exactly — no thickness change
|
|
590
|
-
// between empty and typed states.
|
|
591
|
-
const showCursorHere = focused && !isEmpty && i === cursorLine;
|
|
592
|
-
return (
|
|
593
|
-
<box key={i} height={1}>
|
|
594
|
-
<text>
|
|
595
|
-
<span fg={theme.text}>{line}</span>
|
|
596
|
-
{showCursorHere ? <span bg={theme.primary}> </span> : null}
|
|
597
|
-
</text>
|
|
598
|
-
</box>
|
|
599
|
-
);
|
|
600
|
-
})}
|
|
601
|
-
</box>
|
|
567
|
+
<textarea
|
|
568
|
+
ref={ref}
|
|
569
|
+
initialValue={value}
|
|
570
|
+
placeholder={placeholder}
|
|
571
|
+
focused={focused}
|
|
572
|
+
textColor={theme.text}
|
|
573
|
+
backgroundColor="transparent"
|
|
574
|
+
focusedBackgroundColor="transparent"
|
|
575
|
+
focusedTextColor={theme.text}
|
|
576
|
+
placeholderColor={theme.textDim}
|
|
577
|
+
wrapMode="word"
|
|
578
|
+
flexGrow={1}
|
|
579
|
+
/>
|
|
602
580
|
);
|
|
603
581
|
}
|
|
604
582
|
|
|
@@ -607,33 +585,26 @@ function StringContent({
|
|
|
607
585
|
value,
|
|
608
586
|
placeholder,
|
|
609
587
|
focused,
|
|
588
|
+
onInput,
|
|
610
589
|
}: {
|
|
611
590
|
theme: PickerTheme;
|
|
612
591
|
value: string;
|
|
613
592
|
placeholder: string;
|
|
614
593
|
focused: boolean;
|
|
594
|
+
onInput: (value: string) => void;
|
|
615
595
|
}) {
|
|
616
|
-
const isEmpty = value === "";
|
|
617
|
-
|
|
618
|
-
if (isEmpty) {
|
|
619
|
-
return (
|
|
620
|
-
<box height={1} flexDirection="row">
|
|
621
|
-
<PlaceholderWithCursor
|
|
622
|
-
theme={theme}
|
|
623
|
-
placeholder={placeholder}
|
|
624
|
-
focused={focused}
|
|
625
|
-
/>
|
|
626
|
-
</box>
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
596
|
return (
|
|
631
|
-
<
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
597
|
+
<input
|
|
598
|
+
value={value}
|
|
599
|
+
placeholder={placeholder}
|
|
600
|
+
focused={focused}
|
|
601
|
+
onInput={onInput}
|
|
602
|
+
textColor={theme.text}
|
|
603
|
+
backgroundColor="transparent"
|
|
604
|
+
focusedBackgroundColor="transparent"
|
|
605
|
+
focusedTextColor={theme.text}
|
|
606
|
+
flexGrow={1}
|
|
607
|
+
/>
|
|
637
608
|
);
|
|
638
609
|
}
|
|
639
610
|
|
|
@@ -686,11 +657,15 @@ function Field({
|
|
|
686
657
|
field,
|
|
687
658
|
value,
|
|
688
659
|
focused,
|
|
660
|
+
onInput,
|
|
661
|
+
onTextChangeRef,
|
|
689
662
|
}: {
|
|
690
663
|
theme: PickerTheme;
|
|
691
664
|
field: WorkflowInput;
|
|
692
665
|
value: string;
|
|
693
666
|
focused: boolean;
|
|
667
|
+
onInput: (value: string) => void;
|
|
668
|
+
onTextChangeRef: React.RefObject<((value: string) => void) | null>;
|
|
694
669
|
}) {
|
|
695
670
|
const borderCol = focused ? theme.primary : theme.border;
|
|
696
671
|
const bgCol = focused ? theme.backgroundPanel : theme.backgroundElement;
|
|
@@ -711,7 +686,7 @@ function Field({
|
|
|
711
686
|
flexDirection="column"
|
|
712
687
|
paddingLeft={2}
|
|
713
688
|
paddingRight={2}
|
|
714
|
-
|
|
689
|
+
minHeight={boxHeight}
|
|
715
690
|
justifyContent={field.type === "text" ? "flex-start" : "center"}
|
|
716
691
|
title={` ${field.name} `}
|
|
717
692
|
titleAlignment="left"
|
|
@@ -722,7 +697,7 @@ function Field({
|
|
|
722
697
|
value={value}
|
|
723
698
|
placeholder={field.placeholder ?? ""}
|
|
724
699
|
focused={focused}
|
|
725
|
-
|
|
700
|
+
onChangeRef={onTextChangeRef}
|
|
726
701
|
/>
|
|
727
702
|
) : field.type === "string" ? (
|
|
728
703
|
<StringContent
|
|
@@ -730,6 +705,7 @@ function Field({
|
|
|
730
705
|
value={value}
|
|
731
706
|
placeholder={field.placeholder ?? ""}
|
|
732
707
|
focused={focused}
|
|
708
|
+
onInput={onInput}
|
|
733
709
|
/>
|
|
734
710
|
) : field.type === "enum" ? (
|
|
735
711
|
<EnumContent
|
|
@@ -762,6 +738,8 @@ function InputPhase({
|
|
|
762
738
|
fields,
|
|
763
739
|
values,
|
|
764
740
|
focusedFieldIdx,
|
|
741
|
+
onFieldInput,
|
|
742
|
+
onTextChangeRef,
|
|
765
743
|
}: {
|
|
766
744
|
theme: PickerTheme;
|
|
767
745
|
workflow: WorkflowWithMetadata;
|
|
@@ -769,6 +747,8 @@ function InputPhase({
|
|
|
769
747
|
fields: WorkflowInput[];
|
|
770
748
|
values: Record<string, string>;
|
|
771
749
|
focusedFieldIdx: number;
|
|
750
|
+
onFieldInput: (fieldName: string, value: string) => void;
|
|
751
|
+
onTextChangeRef: React.RefObject<((value: string) => void) | null>;
|
|
772
752
|
}) {
|
|
773
753
|
const isStructured = workflow.inputs.length > 0;
|
|
774
754
|
|
|
@@ -801,11 +781,11 @@ function InputPhase({
|
|
|
801
781
|
<span fg={theme.textDim}>{" · "}</span>
|
|
802
782
|
<span fg={theme.mauve}>{agent}</span>
|
|
803
783
|
<span fg={theme.textDim}>{" · "}</span>
|
|
804
|
-
<span fg={theme[SOURCE_COLOR[workflow.source
|
|
805
|
-
{SOURCE_DISPLAY[workflow.source
|
|
784
|
+
<span fg={theme[SOURCE_COLOR[workflow.source]]}>
|
|
785
|
+
{SOURCE_DISPLAY[workflow.source]}
|
|
806
786
|
</span>
|
|
807
787
|
<span fg={theme.textDim}>
|
|
808
|
-
{" (" + SOURCE_DIR[workflow.source
|
|
788
|
+
{" (" + SOURCE_DIR[workflow.source] + ")"}
|
|
809
789
|
</span>
|
|
810
790
|
</text>
|
|
811
791
|
<box height={1} />
|
|
@@ -840,6 +820,8 @@ function InputPhase({
|
|
|
840
820
|
field={f}
|
|
841
821
|
value={values[f.name] ?? ""}
|
|
842
822
|
focused={i === focusedFieldIdx}
|
|
823
|
+
onInput={(v) => onFieldInput(f.name, v)}
|
|
824
|
+
onTextChangeRef={onTextChangeRef}
|
|
843
825
|
/>
|
|
844
826
|
))}
|
|
845
827
|
</box>
|
|
@@ -865,6 +847,7 @@ function ConfirmModal({
|
|
|
865
847
|
justifyContent="center"
|
|
866
848
|
alignItems="center"
|
|
867
849
|
zIndex={100}
|
|
850
|
+
backgroundColor={theme.background}
|
|
868
851
|
>
|
|
869
852
|
<box
|
|
870
853
|
border
|
|
@@ -914,6 +897,17 @@ function ConfirmModal({
|
|
|
914
897
|
);
|
|
915
898
|
}
|
|
916
899
|
|
|
900
|
+
// Stable hint arrays — no need for useMemo since they never change.
|
|
901
|
+
const PICK_HINTS: { key: string; label: string; dim?: boolean }[] = [
|
|
902
|
+
{ key: "↑↓", label: "navigate" },
|
|
903
|
+
{ key: "↵", label: "select" },
|
|
904
|
+
{ key: "esc", label: "quit" },
|
|
905
|
+
];
|
|
906
|
+
const CONFIRM_HINTS: { key: string; label: string; dim?: boolean }[] = [
|
|
907
|
+
{ key: "y", label: "submit" },
|
|
908
|
+
{ key: "n", label: "cancel" },
|
|
909
|
+
];
|
|
910
|
+
|
|
917
911
|
// Per-agent brand color used as the Header pill background.
|
|
918
912
|
const AGENT_PILL_COLOR: Record<AgentType, keyof PickerTheme> = {
|
|
919
913
|
claude: "warning",
|
|
@@ -998,7 +992,7 @@ function Statusline({
|
|
|
998
992
|
: theme.success;
|
|
999
993
|
|
|
1000
994
|
return (
|
|
1001
|
-
<box height={1} flexDirection="row" backgroundColor={theme.surface}>
|
|
995
|
+
<box height={1} flexDirection="row" backgroundColor={theme.surface} position="relative" zIndex={101}>
|
|
1002
996
|
<box
|
|
1003
997
|
backgroundColor={modeColor}
|
|
1004
998
|
paddingLeft={1}
|
|
@@ -1022,7 +1016,7 @@ function Statusline({
|
|
|
1022
1016
|
|
|
1023
1017
|
<box paddingRight={2} alignItems="center" flexDirection="row">
|
|
1024
1018
|
{hints.map((h, i) => (
|
|
1025
|
-
<box key={
|
|
1019
|
+
<box key={h.key} flexDirection="row">
|
|
1026
1020
|
{i > 0 ? (
|
|
1027
1021
|
<text>
|
|
1028
1022
|
<span fg={theme.textDim}>{" · "}</span>
|
|
@@ -1065,28 +1059,26 @@ export function WorkflowPicker({
|
|
|
1065
1059
|
const [focusedFieldIdx, setFocusedFieldIdx] = useState(0);
|
|
1066
1060
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
1067
1061
|
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
|
|
1071
|
-
// bug on top of the already-jarring thickness change that used to
|
|
1072
|
-
// happen when switching between placeholder and typed-text cursors.
|
|
1073
|
-
// Both issues go away with a stable caret.
|
|
1062
|
+
// Ref-based callback for textarea change notifications. The ref is
|
|
1063
|
+
// stable across renders so the textarea effect doesn't re-attach.
|
|
1064
|
+
const textChangeRef = useRef<((value: string) => void) | null>(null);
|
|
1074
1065
|
|
|
1075
1066
|
const entries = useMemo(() => buildEntries(query, workflows), [query, workflows]);
|
|
1076
1067
|
const rows = useMemo(() => buildRows(entries, query), [entries, query]);
|
|
1077
1068
|
|
|
1069
|
+
// Clamp index when the list shrinks (e.g. typing filters entries out).
|
|
1078
1070
|
useEffect(() => {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
}, [entries.length, entryIdx]);
|
|
1071
|
+
setEntryIdx((i) => Math.min(i, Math.max(0, entries.length - 1)));
|
|
1072
|
+
}, [entries.length]);
|
|
1083
1073
|
|
|
1084
1074
|
const focusedWf = entries[entryIdx]?.workflow;
|
|
1085
1075
|
|
|
1086
|
-
const currentFields
|
|
1087
|
-
focusedWf && focusedWf.inputs.length > 0
|
|
1088
|
-
?
|
|
1089
|
-
: [DEFAULT_PROMPT_INPUT]
|
|
1076
|
+
const currentFields = useMemo<WorkflowInput[]>(
|
|
1077
|
+
() => focusedWf && focusedWf.inputs.length > 0
|
|
1078
|
+
? focusedWf.inputs.slice()
|
|
1079
|
+
: [DEFAULT_PROMPT_INPUT],
|
|
1080
|
+
[focusedWf],
|
|
1081
|
+
);
|
|
1090
1082
|
const currentField = currentFields[focusedFieldIdx];
|
|
1091
1083
|
|
|
1092
1084
|
const invalidFieldIndices = useMemo(() => {
|
|
@@ -1100,16 +1092,45 @@ export function WorkflowPicker({
|
|
|
1100
1092
|
}, [currentFields, fieldValues]);
|
|
1101
1093
|
const isFormValid = invalidFieldIndices.length === 0;
|
|
1102
1094
|
|
|
1095
|
+
// Wire the textarea change callback so field values stay in sync.
|
|
1096
|
+
// The ref is written here (not in a child) so the parent state
|
|
1097
|
+
// always reflects the latest textarea content.
|
|
1098
|
+
const focusedField = currentField;
|
|
1099
|
+
textChangeRef.current = focusedField
|
|
1100
|
+
? (text: string) => {
|
|
1101
|
+
setFieldValues((prev) => ({ ...prev, [focusedField.name]: text }));
|
|
1102
|
+
}
|
|
1103
|
+
: null;
|
|
1104
|
+
|
|
1105
|
+
// Stable callback for field input — the setter is referentially stable.
|
|
1106
|
+
const onFieldInput = useCallback(
|
|
1107
|
+
(name: string, v: string) => setFieldValues((prev) => ({ ...prev, [name]: v })),
|
|
1108
|
+
[],
|
|
1109
|
+
);
|
|
1110
|
+
|
|
1111
|
+
// Stable refs for values read inside the keyboard handler,
|
|
1112
|
+
// preventing stale closures when useKeyboard holds the first callback.
|
|
1113
|
+
const entriesRef = useLatest(entries);
|
|
1114
|
+
const focusedWfRef = useLatest(focusedWf);
|
|
1115
|
+
const fieldValuesRef = useLatest(fieldValues);
|
|
1116
|
+
const isFormValidRef = useLatest(isFormValid);
|
|
1117
|
+
const invalidFieldIndicesRef = useLatest(invalidFieldIndices);
|
|
1118
|
+
const currentFieldsRef = useLatest(currentFields);
|
|
1119
|
+
const currentFieldRef = useLatest(currentField);
|
|
1120
|
+
const phaseRef = useLatest(phase);
|
|
1121
|
+
const confirmOpenRef = useLatest(confirmOpen);
|
|
1122
|
+
|
|
1103
1123
|
useKeyboard((key) => {
|
|
1104
1124
|
if (key.ctrl && key.name === "c") {
|
|
1105
1125
|
onCancel();
|
|
1106
1126
|
return;
|
|
1107
1127
|
}
|
|
1108
1128
|
|
|
1109
|
-
if (
|
|
1129
|
+
if (confirmOpenRef.current) {
|
|
1110
1130
|
if (key.name === "y" || key.name === "return") {
|
|
1111
|
-
|
|
1112
|
-
|
|
1131
|
+
const wf = focusedWfRef.current;
|
|
1132
|
+
if (!wf) return;
|
|
1133
|
+
onSubmit({ workflow: wf, inputs: { ...fieldValuesRef.current } });
|
|
1113
1134
|
return;
|
|
1114
1135
|
}
|
|
1115
1136
|
if (key.name === "n" || key.name === "escape") {
|
|
@@ -1119,7 +1140,7 @@ export function WorkflowPicker({
|
|
|
1119
1140
|
return;
|
|
1120
1141
|
}
|
|
1121
1142
|
|
|
1122
|
-
if (
|
|
1143
|
+
if (phaseRef.current === "pick") {
|
|
1123
1144
|
if (key.name === "escape") {
|
|
1124
1145
|
onCancel();
|
|
1125
1146
|
return;
|
|
@@ -1130,15 +1151,16 @@ export function WorkflowPicker({
|
|
|
1130
1151
|
}
|
|
1131
1152
|
if (key.name === "down" || (key.ctrl && key.name === "j")) {
|
|
1132
1153
|
setEntryIdx((i: number) =>
|
|
1133
|
-
Math.min(
|
|
1154
|
+
Math.min(entriesRef.current.length - 1, i + 1),
|
|
1134
1155
|
);
|
|
1135
1156
|
return;
|
|
1136
1157
|
}
|
|
1137
1158
|
if (key.name === "return") {
|
|
1138
|
-
|
|
1159
|
+
const wf = focusedWfRef.current;
|
|
1160
|
+
if (wf) {
|
|
1139
1161
|
const inputs: WorkflowInput[] =
|
|
1140
|
-
|
|
1141
|
-
? [...
|
|
1162
|
+
wf.inputs.length > 0
|
|
1163
|
+
? [...wf.inputs]
|
|
1142
1164
|
: [DEFAULT_PROMPT_INPUT];
|
|
1143
1165
|
const initial: Record<string, string> = {};
|
|
1144
1166
|
for (const f of inputs) {
|
|
@@ -1152,19 +1174,8 @@ export function WorkflowPicker({
|
|
|
1152
1174
|
}
|
|
1153
1175
|
return;
|
|
1154
1176
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
if (
|
|
1160
|
-
key.sequence &&
|
|
1161
|
-
key.sequence.length === 1 &&
|
|
1162
|
-
!key.ctrl &&
|
|
1163
|
-
!key.meta
|
|
1164
|
-
) {
|
|
1165
|
-
const c = key.sequence;
|
|
1166
|
-
if (c >= " " && c <= "~") setQuery((q: string) => q + c);
|
|
1167
|
-
}
|
|
1177
|
+
// All other keys (typing, backspace, arrows) are handled by the
|
|
1178
|
+
// native <input> component in the FilterBar.
|
|
1168
1179
|
return;
|
|
1169
1180
|
}
|
|
1170
1181
|
|
|
@@ -1174,8 +1185,8 @@ export function WorkflowPicker({
|
|
|
1174
1185
|
return;
|
|
1175
1186
|
}
|
|
1176
1187
|
if (key.ctrl && key.name === "s") {
|
|
1177
|
-
if (!
|
|
1178
|
-
setFocusedFieldIdx(
|
|
1188
|
+
if (!isFormValidRef.current) {
|
|
1189
|
+
setFocusedFieldIdx(invalidFieldIndicesRef.current[0]!);
|
|
1179
1190
|
return;
|
|
1180
1191
|
}
|
|
1181
1192
|
setConfirmOpen(true);
|
|
@@ -1183,79 +1194,51 @@ export function WorkflowPicker({
|
|
|
1183
1194
|
}
|
|
1184
1195
|
if (key.name === "tab") {
|
|
1185
1196
|
setFocusedFieldIdx((i: number) => {
|
|
1186
|
-
const len =
|
|
1197
|
+
const len = currentFieldsRef.current.length;
|
|
1187
1198
|
if (len <= 1) return 0;
|
|
1188
1199
|
return key.shift ? (i - 1 + len) % len : (i + 1) % len;
|
|
1189
1200
|
});
|
|
1190
1201
|
return;
|
|
1191
1202
|
}
|
|
1192
|
-
|
|
1203
|
+
const field = currentFieldRef.current;
|
|
1204
|
+
if (!field) return;
|
|
1193
1205
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1206
|
+
// Enum fields use left/right to cycle values.
|
|
1207
|
+
if (field.type === "enum") {
|
|
1208
|
+
const values = field.values ?? [];
|
|
1196
1209
|
if (values.length === 0) return;
|
|
1197
1210
|
if (key.name === "left" || key.name === "right") {
|
|
1198
1211
|
setFieldValues((prev: Record<string, string>) => {
|
|
1199
|
-
const cur = prev[
|
|
1212
|
+
const cur = prev[field.name] ?? values[0] ?? "";
|
|
1200
1213
|
const idx = Math.max(0, values.indexOf(cur));
|
|
1201
1214
|
const delta = key.name === "left" ? -1 : 1;
|
|
1202
1215
|
const nextIdx = (idx + delta + values.length) % values.length;
|
|
1203
|
-
return { ...prev, [
|
|
1216
|
+
return { ...prev, [field.name]: values[nextIdx] ?? "" };
|
|
1204
1217
|
});
|
|
1205
1218
|
}
|
|
1206
1219
|
return;
|
|
1207
1220
|
}
|
|
1208
1221
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
setFocusedFieldIdx((i: number) =>
|
|
1217
|
-
Math.min(currentFields.length - 1, i + 1),
|
|
1218
|
-
);
|
|
1219
|
-
}
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
if (key.name === "backspace") {
|
|
1223
|
-
setFieldValues((prev: Record<string, string>) => ({
|
|
1224
|
-
...prev,
|
|
1225
|
-
[currentField.name]: (prev[currentField.name] ?? "").slice(0, -1),
|
|
1226
|
-
}));
|
|
1222
|
+
// For string fields, return advances focus to the next field
|
|
1223
|
+
// (the native <input> fires onSubmit, but we handle it here so
|
|
1224
|
+
// the focus-cycling logic stays in one place).
|
|
1225
|
+
if (field.type === "string" && key.name === "return") {
|
|
1226
|
+
setFocusedFieldIdx((i: number) =>
|
|
1227
|
+
Math.min(currentFieldsRef.current.length - 1, i + 1),
|
|
1228
|
+
);
|
|
1227
1229
|
return;
|
|
1228
1230
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
key.sequence.length === 1 &&
|
|
1232
|
-
!key.ctrl &&
|
|
1233
|
-
!key.meta
|
|
1234
|
-
) {
|
|
1235
|
-
const c = key.sequence;
|
|
1236
|
-
if (c >= " " && c <= "~") {
|
|
1237
|
-
setFieldValues((prev: Record<string, string>) => ({
|
|
1238
|
-
...prev,
|
|
1239
|
-
[currentField.name]: (prev[currentField.name] ?? "") + c,
|
|
1240
|
-
}));
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1231
|
+
// All other keys for string/text fields (typing, backspace,
|
|
1232
|
+
// arrows, undo/redo) are handled by native <input>/<textarea>.
|
|
1243
1233
|
});
|
|
1244
1234
|
|
|
1245
|
-
const pickHints =
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
{ key: "esc", label: "quit" },
|
|
1249
|
-
];
|
|
1250
|
-
const promptHints = [
|
|
1235
|
+
const pickHints = PICK_HINTS;
|
|
1236
|
+
const confirmHints = CONFIRM_HINTS;
|
|
1237
|
+
const promptHints = useMemo(() => [
|
|
1251
1238
|
{ key: "tab", label: "to navigate forward" },
|
|
1252
1239
|
{ key: "shift+tab", label: "to navigate backward" },
|
|
1253
1240
|
{ key: "ctrl+s", label: "to run", dim: !isFormValid },
|
|
1254
|
-
];
|
|
1255
|
-
const confirmHints = [
|
|
1256
|
-
{ key: "y", label: "submit" },
|
|
1257
|
-
{ key: "n", label: "cancel" },
|
|
1258
|
-
];
|
|
1241
|
+
], [isFormValid]);
|
|
1259
1242
|
|
|
1260
1243
|
const hints = confirmOpen
|
|
1261
1244
|
? confirmHints
|
|
@@ -1288,7 +1271,7 @@ export function WorkflowPicker({
|
|
|
1288
1271
|
paddingTop={1}
|
|
1289
1272
|
>
|
|
1290
1273
|
<box width={36} flexDirection="column">
|
|
1291
|
-
<FilterBar theme={theme} query={query} />
|
|
1274
|
+
<FilterBar theme={theme} query={query} focused={phase === "pick"} onInput={setQuery} />
|
|
1292
1275
|
<box height={1} />
|
|
1293
1276
|
<WorkflowList
|
|
1294
1277
|
theme={theme}
|
|
@@ -1312,7 +1295,9 @@ export function WorkflowPicker({
|
|
|
1312
1295
|
agent={agent}
|
|
1313
1296
|
fields={currentFields}
|
|
1314
1297
|
values={fieldValues}
|
|
1315
|
-
focusedFieldIdx={focusedFieldIdx}
|
|
1298
|
+
focusedFieldIdx={confirmOpen ? -1 : focusedFieldIdx}
|
|
1299
|
+
onFieldInput={onFieldInput}
|
|
1300
|
+
onTextChangeRef={textChangeRef}
|
|
1316
1301
|
/>
|
|
1317
1302
|
) : null}
|
|
1318
1303
|
|