@bastani/atomic 0.8.14-0 → 0.8.15-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.
Files changed (50) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +0 -8
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/CHANGELOG.md +3 -0
  5. package/dist/builtin/mcp/index.ts +4 -8
  6. package/dist/builtin/mcp/package.json +1 -1
  7. package/dist/builtin/subagents/package.json +1 -1
  8. package/dist/builtin/subagents/skills/tmux/SKILL.md +220 -0
  9. package/dist/builtin/subagents/skills/tmux/scripts/find-sessions.sh +112 -0
  10. package/dist/builtin/subagents/skills/tmux/scripts/wait-for-text.sh +83 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +10 -1
  13. package/dist/builtin/workflows/README.md +3 -1
  14. package/dist/builtin/workflows/builtin/ralph.ts +222 -295
  15. package/dist/builtin/workflows/package.json +1 -1
  16. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +20 -11
  17. package/dist/builtin/workflows/src/extension/index.ts +1 -0
  18. package/dist/builtin/workflows/src/extension/status-writer.ts +18 -3
  19. package/dist/builtin/workflows/src/runs/background/runner.ts +8 -10
  20. package/dist/builtin/workflows/src/runs/foreground/executor.ts +484 -91
  21. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +13 -2
  22. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +41 -15
  23. package/dist/builtin/workflows/src/runs/shared/graph-inference.ts +31 -0
  24. package/dist/builtin/workflows/src/runs/shared/prompt-callsite.ts +98 -0
  25. package/dist/builtin/workflows/src/shared/persistence-restore.ts +3 -1
  26. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
  27. package/dist/builtin/workflows/src/shared/store-types.ts +12 -1
  28. package/dist/builtin/workflows/src/shared/store.ts +77 -3
  29. package/dist/builtin/workflows/src/tui/graph-view.ts +17 -1
  30. package/dist/builtin/workflows/src/tui/prompt-card.ts +185 -30
  31. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +386 -21
  32. package/docs/changelog.mdx +41 -14
  33. package/docs/docs.json +1 -0
  34. package/docs/extensions.md +19 -19
  35. package/docs/images/workflow-input-picker.png +0 -0
  36. package/docs/images/workflow-list.png +0 -0
  37. package/docs/index.md +33 -27
  38. package/docs/providers.md +2 -2
  39. package/docs/quickstart.md +15 -15
  40. package/docs/sdk.md +8 -8
  41. package/docs/sessions.md +5 -5
  42. package/docs/settings.md +27 -1
  43. package/docs/skills.md +2 -2
  44. package/docs/subagents.md +157 -0
  45. package/docs/usage.md +7 -7
  46. package/docs/windows.md +8 -0
  47. package/docs/workflows.md +62 -9
  48. package/package.json +2 -1
  49. package/docs/images/doom-extension.png +0 -0
  50. package/docs/images/exy.png +0 -3
@@ -47,7 +47,7 @@ import {
47
47
  type ChatMessageRenderOptions,
48
48
  type ReadonlyFooterDataProvider,
49
49
  } from "@bastani/atomic";
50
- import { Box, Text } from "@earendil-works/pi-tui";
50
+ import { Box, Editor, Text } from "@earendil-works/pi-tui";
51
51
  import type {
52
52
  Component,
53
53
  EditorComponent,
@@ -63,9 +63,10 @@ import {
63
63
  type StageCustomUiRequest,
64
64
  type StageUiBroker,
65
65
  } from "../shared/stage-ui-broker.js";
66
- import type { StageNotice, StageSnapshot } from "../shared/store-types.js";
66
+ import type { PendingPrompt, StageNotice, StageSnapshot, StageStatus } from "../shared/store-types.js";
67
67
  import type { GraphTheme } from "./graph-theme.js";
68
68
  import type { StageControlHandle } from "../runs/foreground/stage-control-registry.js";
69
+ import { isKeybindingsLike } from "./keybindings-adapter.js";
69
70
  import { BOLD, RESET, hexBg, hexToAnsi, lerpColor } from "./color-utils.js";
70
71
  import { Key, matchesKey, visibleWidth } from "./text-helpers.js";
71
72
  import {
@@ -73,11 +74,23 @@ import {
73
74
  planStageChatFrame,
74
75
  resolveStageChatViewportRows,
75
76
  } from "./stage-chat-layout.js";
77
+ import {
78
+ createPromptCardState,
79
+ defaultResponseFor,
80
+ handlePromptCardInput,
81
+ renderPromptCard,
82
+ type PromptCardState,
83
+ } from "./prompt-card.js";
84
+ import { renderRoundedBoxLines } from "./chat-surface.js";
76
85
 
77
86
  // ---------------------------------------------------------------------------
78
87
  // Options & types
79
88
  // ---------------------------------------------------------------------------
80
89
 
90
+ function isReadOnlyArchiveStatus(status: StageStatus): boolean {
91
+ return status === "completed" || status === "failed" || status === "skipped";
92
+ }
93
+
81
94
  export interface StageChatViewOpts {
82
95
  store: Store;
83
96
  graphTheme: GraphTheme;
@@ -151,6 +164,7 @@ type AgentSnapshotMessage = AgentSession["messages"][number];
151
164
  * overrides this by passing `getViewportRows()`.
152
165
  */
153
166
  const VIEW_LINE_COUNT = 32;
167
+ const PROMPT_SCROLL_STEP_ROWS = 4;
154
168
 
155
169
  /** Header strip — ` STAGE wf / stage <meta> ● status` without a leading marker glyph. */
156
170
  const HEADER_ROWS = 1;
@@ -180,9 +194,15 @@ export class StageChatView implements Component, Focusable {
180
194
  private piTui?: TUI;
181
195
  private piTheme?: unknown;
182
196
  private piKeybindings?: unknown;
197
+ private piEditorFactory?: StageChatViewOpts["piEditorFactory"];
183
198
  private chatHost: ChatSessionHost<NoticeEntry>;
184
199
  private stageUiBroker: StageUiBroker;
185
200
  private mountedCustomUi: MountedStageCustomUi | null = null;
201
+ private promptState: PromptCardState | null = null;
202
+ private promptEditor: EditorComponent | null = null;
203
+ private promptEditorPromptId: string | null = null;
204
+ private promptScrollOffset = 0;
205
+ private promptMaxScroll = 0;
186
206
  private getChatRenderSettings?: () =>
187
207
  | Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">>
188
208
  | undefined;
@@ -211,6 +231,7 @@ export class StageChatView implements Component, Focusable {
211
231
  this.piTui = opts.piTui;
212
232
  this.piTheme = opts.piTheme;
213
233
  this.piKeybindings = opts.piKeybindings;
234
+ this.piEditorFactory = opts.piEditorFactory;
214
235
  this.getChatRenderSettings = opts.getChatRenderSettings;
215
236
  this.footerData = opts.footerData;
216
237
  this.stageUiBroker = opts.stageUiBroker ?? stageUiBroker;
@@ -299,6 +320,7 @@ export class StageChatView implements Component, Focusable {
299
320
  const initialStage = this._currentStage();
300
321
  this._snapshotMessagesFromSessionFile(initialStage);
301
322
  this._absorbStageNotices(initialStage);
323
+ this._syncPromptState(initialStage?.pendingPrompt);
302
324
 
303
325
  this._unsubscribeStore = this.store.subscribe(() => {
304
326
  const stage = this._currentStage();
@@ -314,6 +336,7 @@ export class StageChatView implements Component, Focusable {
314
336
  // `stage.setModel`, `stage.compact`, …) so they thread through the
315
337
  // transcript without a special render path.
316
338
  changed = this._absorbStageNotices(stage) || changed;
339
+ changed = this._syncPromptState(stage?.pendingPrompt) || changed;
317
340
  this.chatHost.syncAnimationTick();
318
341
  if (changed) this.requestRender?.();
319
342
  });
@@ -416,6 +439,68 @@ export class StageChatView implements Component, Focusable {
416
439
  return run?.stages.find((s) => s.id === this.stageId);
417
440
  }
418
441
 
442
+ private _syncPromptState(prompt: PendingPrompt | undefined): boolean {
443
+ if (!prompt) {
444
+ const changed = this.promptState !== null;
445
+ this.promptState = null;
446
+ this._disposePromptEditor();
447
+ this.promptScrollOffset = 0;
448
+ this.promptMaxScroll = 0;
449
+ return changed;
450
+ }
451
+ if (!this.promptState || this.promptState.prompt.id !== prompt.id) {
452
+ this.promptState = createPromptCardState(prompt);
453
+ this._resetPromptEditor(prompt);
454
+ this.promptScrollOffset = 0;
455
+ this.promptMaxScroll = 0;
456
+ return true;
457
+ }
458
+ return false;
459
+ }
460
+
461
+ private _resetPromptEditor(prompt: PendingPrompt): void {
462
+ this._disposePromptEditor();
463
+ if ((prompt.kind !== "input" && prompt.kind !== "editor") || !this.piTui) return;
464
+ const editor = this.piEditorFactory
465
+ ? this.piEditorFactory(this.piTui, editorThemeFromGraphTheme(this.theme), this.piKeybindings)
466
+ : new Editor(this.piTui, editorThemeFromGraphTheme(this.theme), { paddingX: 0 });
467
+ editor.setText(typeof prompt.initial === "string" ? prompt.initial : "");
468
+ setEditorPlaceholder(editor, "Type your response…");
469
+ setEditorBorderColor(editor, (text) => hexToAnsi(this.theme.accent) + text + RESET);
470
+ editor.onChange = (text: string) => {
471
+ if (this.promptState?.prompt.id !== prompt.id) return;
472
+ this.promptState.rawText = text;
473
+ this.promptState.caret = text.length;
474
+ this.requestRender?.();
475
+ };
476
+ editor.onSubmit = (text: string) => {
477
+ this._resolvePromptResponse(prompt.id, text);
478
+ };
479
+ this.promptEditor = editor;
480
+ this.promptEditorPromptId = prompt.id;
481
+ }
482
+
483
+ private _disposePromptEditor(): void {
484
+ const editor = this.promptEditor;
485
+ this.promptEditor = null;
486
+ this.promptEditorPromptId = null;
487
+ const disposable = editor as (EditorComponent & { dispose?: () => void }) | null;
488
+ disposable?.dispose?.();
489
+ }
490
+
491
+ private _resolvePromptResponse(promptId: string, response: unknown): void {
492
+ const prompt = this.promptState?.prompt;
493
+ if (!prompt || prompt.id !== promptId) return;
494
+ this.promptState = null;
495
+ this._disposePromptEditor();
496
+ // A false return means the prompt was already resolved/removed (for
497
+ // example by run abort). The local UI is already stale, so clearing it is
498
+ // the least surprising recovery path.
499
+ this.store.resolveStagePendingPrompt(this.runId, this.stageId, prompt.id, response);
500
+ this.requestRender?.();
501
+ this.onDetach();
502
+ }
503
+
419
504
  // -------------------------------------------------------------------------
420
505
  // Frame sizing
421
506
  // -------------------------------------------------------------------------
@@ -449,13 +534,13 @@ export class StageChatView implements Component, Focusable {
449
534
  private _isPaused(
450
535
  stage: StageSnapshot | undefined = this._currentStage(),
451
536
  ): boolean {
452
- return this.localPaused || stage?.status === "paused";
537
+ return this.localPaused || stage?.status === "paused" || this._liveHandle()?.status === "paused";
453
538
  }
454
539
 
455
540
  private _isReadOnlyArchive(stage: StageSnapshot | undefined = this._currentStage()): boolean {
456
541
  if (this._liveHandle()) return false;
457
542
  if (!stage) return true;
458
- return stage.status === "completed" || stage.status === "failed" || Boolean(stage.sessionFile);
543
+ return isReadOnlyArchiveStatus(stage.status) || Boolean(stage.sessionFile);
459
544
  }
460
545
 
461
546
  private async _handleSlashCommand(text: string): Promise<boolean> {
@@ -491,12 +576,15 @@ export class StageChatView implements Component, Focusable {
491
576
  const headerLines = this._renderHeader(w, stage);
492
577
  const sepLines = [this._sepRule(w)];
493
578
  const customUiActive = this.mountedCustomUi !== null;
579
+ this._syncPromptState(stage?.pendingPrompt);
580
+ const promptActive = !customUiActive && this.promptState !== null;
494
581
  const readOnlyArchive = this._isReadOnlyArchive(stage);
495
- const pendingLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderPendingMessages(w);
496
- const workingLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderWorkingStatus(w);
497
- const usageLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderUsage(w);
498
- const editorLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderEditor(w);
499
- const footerLines = customUiActive || readOnlyArchive ? [] : this.chatHost.renderFooter(w);
582
+ const chatChromeHidden = customUiActive || promptActive || readOnlyArchive;
583
+ const pendingLines = chatChromeHidden ? [] : this.chatHost.renderPendingMessages(w);
584
+ const workingLines = chatChromeHidden ? [] : this.chatHost.renderWorkingStatus(w);
585
+ const usageLines = chatChromeHidden ? [] : this.chatHost.renderUsage(w);
586
+ const editorLines = chatChromeHidden ? [] : this.chatHost.renderEditor(w);
587
+ const footerLines = chatChromeHidden ? [] : this.chatHost.renderFooter(w);
500
588
 
501
589
  const totalRows = this._viewLineCount();
502
590
  const plan = planStageChatFrame({
@@ -516,13 +604,20 @@ export class StageChatView implements Component, Focusable {
516
604
  const visibleFooterLines = footerLines.slice(0, plan.footerRows);
517
605
  const bodyBudget = plan.bodyRows;
518
606
  if (blocked) this.chatHost.scrollToBottom();
519
- const bodyLines = customUiActive
520
- ? this._renderCustomUiBody(w, bodyBudget)
521
- : blocked
522
- ? this._renderBlockedBody(w, bodyBudget, stage)
523
- : readOnlyArchive
524
- ? this._renderReadOnlyArchiveBody(w, bodyBudget, stage)
525
- : this.chatHost.renderBody(w, bodyBudget);
607
+
608
+ let bodyLines: string[];
609
+ if (customUiActive) {
610
+ bodyLines = this._renderCustomUiBody(w, bodyBudget);
611
+ } else if (promptActive) {
612
+ bodyLines = this._renderPromptBody(w, bodyBudget);
613
+ } else if (blocked) {
614
+ bodyLines = this._renderBlockedBody(w, bodyBudget, stage);
615
+ } else if (readOnlyArchive) {
616
+ bodyLines = this._renderReadOnlyArchiveBody(w, bodyBudget, stage);
617
+ } else {
618
+ bodyLines = this.chatHost.renderBody(w, bodyBudget);
619
+ }
620
+
526
621
  const lines = [
527
622
  ...headerLines,
528
623
  ...sepLines,
@@ -592,6 +687,10 @@ export class StageChatView implements Component, Focusable {
592
687
  budget: number,
593
688
  stage: StageSnapshot | undefined,
594
689
  ): string[] {
690
+ if (stage?.promptFootprint) {
691
+ return this._renderReadOnlyPromptArchiveBody(width, budget, stage);
692
+ }
693
+
595
694
  const t = this.theme;
596
695
  const calloutRows = 6;
597
696
  const transcriptBudget = Math.max(1, budget - calloutRows);
@@ -631,6 +730,78 @@ export class StageChatView implements Component, Focusable {
631
730
  return lines;
632
731
  }
633
732
 
733
+ private _renderReadOnlyPromptArchiveBody(
734
+ width: number,
735
+ budget: number,
736
+ stage: StageSnapshot,
737
+ ): string[] {
738
+ const t = this.theme;
739
+ const prompt = stage.promptFootprint;
740
+ if (!prompt) return this._fitBodyLines([], width, budget);
741
+
742
+ const innerWidth = Math.max(2, width - 2);
743
+ const bodyLines: string[] = [];
744
+ const messageBox = new Box(2, 1);
745
+ messageBox.addChild(new Text(paint(prompt.message, t.text), 0, 0));
746
+ bodyLines.push(...messageBox.render(innerWidth));
747
+ bodyLines.push(...new Text(paint("prompt type", t.textMuted, { bold: true }) + paint(` ${prompt.kind}`, t.text), 2, 0).render(innerWidth));
748
+
749
+ if (prompt.kind === "select" && prompt.choices && prompt.choices.length > 0) {
750
+ bodyLines.push(...new Text(paint("choices", t.textMuted, { bold: true }), 2, 0).render(innerWidth));
751
+ for (const choice of prompt.choices) {
752
+ bodyLines.push(...new Text(paint("• ", t.dim) + paint(choice, t.text), 4, 0).render(innerWidth));
753
+ }
754
+ } else if (prompt.kind === "confirm") {
755
+ bodyLines.push(...new Text(paint("choices", t.textMuted, { bold: true }) + paint(" yes / no", t.text), 2, 0).render(innerWidth));
756
+ }
757
+
758
+ if ((prompt.kind === "input" || prompt.kind === "editor") && prompt.initial && prompt.initial.length > 0) {
759
+ bodyLines.push(...new Text(paint("initial value shown", t.textMuted, { bold: true }), 2, 0).render(innerWidth));
760
+ bodyLines.push(...new Text(paint(prompt.initial, t.dim), 4, 0).render(innerWidth));
761
+ }
762
+
763
+ const answer = this._readOnlyPromptAnswer(stage, prompt);
764
+ bodyLines.push("");
765
+ bodyLines.push(...new Text(paint("your response", t.textMuted, { bold: true }), 2, 0).render(innerWidth));
766
+ bodyLines.push(...new Text(paint(answer, answer.startsWith("(") ? t.dim : t.text), 4, 0).render(innerWidth));
767
+ bodyLines.push(...new Text(
768
+ paint("esc", t.accent, { bold: true }) +
769
+ paint(" close", t.textMuted) +
770
+ paint(" · ", t.dim) +
771
+ paint("ctrl+d", t.accent, { bold: true }) +
772
+ paint(" return to graph", t.textMuted),
773
+ 2,
774
+ 0,
775
+ ).render(innerWidth));
776
+
777
+ const title = stage.status === "skipped" ? "QUESTION SKIPPED" : "QUESTION ASKED";
778
+ const cardLines = renderRoundedBoxLines({
779
+ title,
780
+ bodyLines,
781
+ width,
782
+ theme: t,
783
+ accent: t.border,
784
+ });
785
+ return this._fitPromptBodyLines(cardLines, width, budget);
786
+ }
787
+
788
+ private _readOnlyPromptAnswer(stage: StageSnapshot, prompt: PendingPrompt): string {
789
+ const answer = this.store.getStagePromptAnswer(this.runId, stage.id);
790
+ if (answer && answer.promptId === prompt.id) {
791
+ return formatReadOnlyPromptAnswer(answer.value, prompt.kind);
792
+ }
793
+ switch (stage.promptAnswerState) {
794
+ case "ambiguous":
795
+ return "(response replay is ambiguous)";
796
+ case "unavailable":
797
+ return "(response unavailable)";
798
+ case "available":
799
+ return "(response no longer in live memory)";
800
+ default:
801
+ return "(no response saved)";
802
+ }
803
+ }
804
+
634
805
  private _renderBlockedBody(
635
806
  width: number,
636
807
  budget: number,
@@ -677,6 +848,62 @@ export class StageChatView implements Component, Focusable {
677
848
  const component = this.mountedCustomUi?.component;
678
849
  if (component) setComponentFocused(component, this.focused);
679
850
  const lines = component ? component.render(width) : [];
851
+ return this._fitBodyLines(lines, width, budget);
852
+ }
853
+
854
+ private _renderPromptBody(width: number, budget: number): string[] {
855
+ const primitiveLines = this._renderPrimitivePromptBody(width);
856
+ if (primitiveLines) return this._fitPromptBodyLines(primitiveLines, width, budget);
857
+
858
+ const state = this.promptState;
859
+ const lines = state
860
+ ? renderPromptCard({
861
+ state,
862
+ theme: this.theme,
863
+ width,
864
+ cursorOn: this.focused,
865
+ })
866
+ : [];
867
+ return this._fitPromptBodyLines(lines, width, budget);
868
+ }
869
+
870
+ private _renderPrimitivePromptBody(width: number): string[] | null {
871
+ const state = this.promptState;
872
+ const editor = this.promptEditor;
873
+ if (!state || !editor) return null;
874
+ setEditorFocused(editor, this.focused);
875
+ setEditorBorderColor(editor, (text) => hexToAnsi(this.theme.accent) + text + RESET);
876
+
877
+ const innerWidth = Math.max(2, width - 2);
878
+ const bodyLines: string[] = [];
879
+ const messageBox = new Box(2, 1);
880
+ messageBox.addChild(new Text(paint(state.prompt.message, this.theme.text), 0, 0));
881
+ bodyLines.push(...messageBox.render(innerWidth));
882
+ bodyLines.push(...new Text(paint("response", this.theme.textMuted, { bold: true }), 2, 0).render(innerWidth));
883
+ for (const line of editor.render(Math.max(20, innerWidth - 4))) {
884
+ bodyLines.push(" " + line);
885
+ }
886
+ bodyLines.push("");
887
+ bodyLines.push(...new Text(renderHintsForPrompt(state.prompt.kind, this.theme), 2, 0).render(innerWidth));
888
+
889
+ return renderRoundedBoxLines({
890
+ title: "AWAITING INPUT",
891
+ bodyLines,
892
+ width,
893
+ theme: this.theme,
894
+ accent: this.theme.border,
895
+ });
896
+ }
897
+
898
+ private _fitPromptBodyLines(lines: readonly string[], width: number, budget: number): string[] {
899
+ this.promptMaxScroll = Math.max(0, lines.length - budget);
900
+ this.promptScrollOffset = Math.max(0, Math.min(this.promptScrollOffset, this.promptMaxScroll));
901
+ const framed = lines.slice(this.promptScrollOffset, this.promptScrollOffset + budget);
902
+ while (framed.length < budget) framed.push(this._blank(width));
903
+ return framed;
904
+ }
905
+
906
+ private _fitBodyLines(lines: readonly string[], width: number, budget: number): string[] {
680
907
  const framed = lines.slice(0, budget);
681
908
  while (framed.length < budget) framed.push(this._blank(width));
682
909
  return framed;
@@ -778,6 +1005,35 @@ export class StageChatView implements Component, Focusable {
778
1005
  return true;
779
1006
  }
780
1007
 
1008
+ private _handlePromptInput(data: string): void {
1009
+ const state = this.promptState;
1010
+ if (!state) return;
1011
+ if (this.promptEditor && this.promptEditorPromptId === state.prompt.id) {
1012
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
1013
+ this._resolvePromptResponse(state.prompt.id, defaultResponseFor(state.prompt));
1014
+ return;
1015
+ }
1016
+ setEditorFocused(this.promptEditor, this.focused);
1017
+ this.promptEditor.handleInput(data);
1018
+ this.requestRender?.();
1019
+ return;
1020
+ }
1021
+ const action = handlePromptCardInput(
1022
+ data,
1023
+ state,
1024
+ isKeybindingsLike(this.piKeybindings) ? this.piKeybindings : undefined,
1025
+ );
1026
+ if (action.kind === "noop") {
1027
+ this.requestRender?.();
1028
+ return;
1029
+ }
1030
+ const prompt = state.prompt;
1031
+ const response = action.kind === "submit"
1032
+ ? action.response
1033
+ : defaultResponseFor(prompt);
1034
+ this._resolvePromptResponse(prompt.id, response);
1035
+ }
1036
+
781
1037
  // -------------------------------------------------------------------------
782
1038
  // Input
783
1039
  // -------------------------------------------------------------------------
@@ -800,15 +1056,21 @@ export class StageChatView implements Component, Focusable {
800
1056
  this.requestRender?.();
801
1057
  return true;
802
1058
  }
803
- if (this.chatHost.handleScrollInput(data)) {
804
- return true;
805
- }
1059
+ this._syncPromptState(this._currentStage()?.pendingPrompt);
806
1060
  if (matchesKey(data, Key.ctrl("d"))) {
807
- if (this.chatHost.hasInputText()) return this.chatHost.handleInput(data);
1061
+ if (!this.promptState && this.chatHost.hasInputText()) return this.chatHost.handleInput(data);
808
1062
  if (this._isPaused()) this.onClose();
809
1063
  else this.onDetach();
810
1064
  return true;
811
1065
  }
1066
+ if (this.promptState) {
1067
+ if (this._handlePromptScrollInput(data, this.promptEditor === null)) return true;
1068
+ this._handlePromptInput(data);
1069
+ return true;
1070
+ }
1071
+ if (this.chatHost.handleScrollInput(data)) {
1072
+ return true;
1073
+ }
812
1074
  if (matchesKey(data, Key.escape)) {
813
1075
  if (
814
1076
  this._isStreaming() ||
@@ -836,8 +1098,70 @@ export class StageChatView implements Component, Focusable {
836
1098
  return this.chatHost.handleInput(data);
837
1099
  }
838
1100
 
1101
+ private _handlePromptScrollInput(data: string, includeKeyboard = true): boolean {
1102
+ const wheelDeltaRows = this._mouseWheelDeltaRows(data);
1103
+ if (wheelDeltaRows !== 0) {
1104
+ this._scrollPromptBy(wheelDeltaRows);
1105
+ return true;
1106
+ }
1107
+ if (this._isMouseSequence(data)) return true;
1108
+ if (!includeKeyboard) return false;
1109
+ if (matchesKey(data, "pageUp")) {
1110
+ this._scrollPromptBy(-this._promptPageSize());
1111
+ return true;
1112
+ }
1113
+ if (matchesKey(data, "pageDown")) {
1114
+ this._scrollPromptBy(this._promptPageSize());
1115
+ return true;
1116
+ }
1117
+ if (!this.promptEditor && matchesKey(data, "home")) {
1118
+ this.promptScrollOffset = 0;
1119
+ this.requestRender?.();
1120
+ return true;
1121
+ }
1122
+ if (!this.promptEditor && matchesKey(data, "end")) {
1123
+ this.promptScrollOffset = this.promptMaxScroll;
1124
+ this.requestRender?.();
1125
+ return true;
1126
+ }
1127
+ return false;
1128
+ }
1129
+
1130
+ private _scrollPromptBy(deltaRows: number): void {
1131
+ this.promptScrollOffset = Math.max(
1132
+ 0,
1133
+ Math.min(this.promptMaxScroll, this.promptScrollOffset + deltaRows),
1134
+ );
1135
+ this.requestRender?.();
1136
+ }
1137
+
1138
+ private _promptPageSize(): number {
1139
+ return Math.max(4, this._viewLineCount() - HEADER_ROWS - SEP_ROWS - 2);
1140
+ }
1141
+
1142
+ private _mouseWheelDeltaRows(data: string): number {
1143
+ const sgr = data.match(/^\x1b\[<(\d+);\d+;\d+M$/);
1144
+ if (sgr) return this._wheelDeltaForButtonCode(Number.parseInt(sgr[1]!, 10));
1145
+ if (data.startsWith("\x1b[M") && data.length >= 6) {
1146
+ return this._wheelDeltaForButtonCode(data.charCodeAt(3) - 32);
1147
+ }
1148
+ return 0;
1149
+ }
1150
+
1151
+ private _wheelDeltaForButtonCode(code: number): number {
1152
+ if ((code & 64) === 0) return 0;
1153
+ const direction = code & 3;
1154
+ if (direction === 0) return -PROMPT_SCROLL_STEP_ROWS;
1155
+ if (direction === 1) return PROMPT_SCROLL_STEP_ROWS;
1156
+ return 0;
1157
+ }
1158
+
1159
+ private _isMouseSequence(data: string): boolean {
1160
+ return /^\x1b\[<\d+;\d+;\d+[mM]$/.test(data) || data.startsWith("\x1b[M");
1161
+ }
1162
+
839
1163
  invalidate(): void {
840
- // Stateless render reads directly from snapshot + handle.
1164
+ this._syncPromptState(this._currentStage()?.pendingPrompt);
841
1165
  }
842
1166
 
843
1167
  dispose(): void {
@@ -846,6 +1170,7 @@ export class StageChatView implements Component, Focusable {
846
1170
  this._unsubscribeHandle?.();
847
1171
  this._unsubscribeHandle = null;
848
1172
  this._rejectMountedCustomUi("stage chat view disposed");
1173
+ this._disposePromptEditor();
849
1174
  this._unregisterStageUiHost?.();
850
1175
  this._unregisterStageUiHost = null;
851
1176
  this.chatHost.dispose();
@@ -905,6 +1230,18 @@ interface TranscriptDebugEntry {
905
1230
  readonly output: string;
906
1231
  }
907
1232
 
1233
+ function formatReadOnlyPromptAnswer(value: unknown, kind: PendingPrompt["kind"]): string {
1234
+ if (kind === "confirm") return value === true ? "yes" : "no";
1235
+ if (typeof value === "string") return value.length > 0 ? value : "(empty response)";
1236
+ if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
1237
+ try {
1238
+ const encoded = JSON.stringify(value);
1239
+ return encoded ?? String(value);
1240
+ } catch {
1241
+ return String(value);
1242
+ }
1243
+ }
1244
+
908
1245
  function transcriptDebugEntries(entry: TranscriptEntry): TranscriptDebugEntry[] {
909
1246
  if (isChatMessageEntry(entry) && entry.kind === "assistant") {
910
1247
  const entries: TranscriptDebugEntry[] = [];
@@ -993,6 +1330,27 @@ function setComponentFocused(component: Component, focused: boolean): void {
993
1330
  if ("focused" in candidate) candidate.focused = focused;
994
1331
  }
995
1332
 
1333
+ function setEditorFocused(editor: EditorComponent, focused: boolean): void {
1334
+ setComponentFocused(editor, focused);
1335
+ }
1336
+
1337
+ function setEditorPlaceholder(editor: EditorComponent, placeholder: string | undefined): void {
1338
+ const candidate = editor as EditorComponent & {
1339
+ setPlaceholder?: (value: string | undefined) => void;
1340
+ };
1341
+ candidate.setPlaceholder?.(placeholder);
1342
+ }
1343
+
1344
+ function setEditorBorderColor(
1345
+ editor: EditorComponent,
1346
+ borderColor: (text: string) => string,
1347
+ ): void {
1348
+ const candidate = editor as EditorComponent & {
1349
+ borderColor?: (text: string) => string;
1350
+ };
1351
+ if ("borderColor" in candidate) candidate.borderColor = borderColor;
1352
+ }
1353
+
996
1354
  function isChatMessageEntry(entry: TranscriptEntry): entry is ChatMessageEntry {
997
1355
  return "kind" in entry && entry.role !== "notice";
998
1356
  }
@@ -1083,6 +1441,13 @@ function paint(text: string, fg: string, opts: PaintOpts = {}): string {
1083
1441
  return out + text + RESET;
1084
1442
  }
1085
1443
 
1444
+ function renderHintsForPrompt(kind: PendingPrompt["kind"], theme: GraphTheme): string {
1445
+ if (kind === "input" || kind === "editor") {
1446
+ return `${paint("enter", theme.textMuted, { bold: true })} Submit · ${paint("esc/ctrl+c", theme.textMuted, { bold: true })} Skip`;
1447
+ }
1448
+ return `${paint("enter", theme.textMuted, { bold: true })} Select · ${paint("esc/ctrl+c", theme.textMuted, { bold: true })} Skip`;
1449
+ }
1450
+
1086
1451
  /**
1087
1452
  * Foreground styling for text that will be wrapped by a `Box` background.
1088
1453
  * A normal `RESET` would also clear the parent background, so close only the
@@ -3,27 +3,54 @@ title: "Changelog"
3
3
  description: "What's new in Atomic"
4
4
  ---
5
5
 
6
- <Update label="May 22, 2026" tags={["v0.8.13"]}>
6
+ <Update label="May 25, 2026" tags={["v0.8.14"]}>
7
+
8
+ ## Stable release
9
+
10
+ - **0.8.14 is the stable promotion of the 0.8.14 prerelease.** It keeps the upstream Pi sync, bundled workflow documentation refresh, and bundled subagent guidance updates from the prerelease.
11
+ - **Synced with upstream Pi patches.** Atomic's coding-agent fork is now aligned with upstream Pi patches since v0.75.4 and bundles Pi libraries at 0.75.5.
12
+ - **Bundled workflow docs are current.** The docs now describe Ralph's final PR-preparation phase, deep-research report artifacts, and newer workflow inspection/control actions such as `stages`, `stage`, `transcript`, `send`, `pause`, and `reload`. See [Workflows](/workflows).
13
+ - **Bundled subagent guidance is easier to find.** Atomic now documents how to use bundled subagents for delegation, background work, nested fanout boundaries, model fallbacks, and custom agent guardrails. See [Subagents](/subagents).
14
+
15
+ ## Fixes
16
+
17
+ - Managed extension installs, git package ref reconciliation, async file tools, export HTML escaping, OpenCode session headers, OAuth device-code login, footer path abbreviation, clipboard native loading, and collapsed read output rendering received upstream fixes.
18
+ - The built-in header model label now refreshes when you change models, matching the footer below the chat box.
19
+
20
+ </Update>
21
+
22
+ <Update label="May 21, 2026" tags={["v0.8.13"]}>
23
+
24
+ ## Stable release
25
+
26
+ - **0.8.13 is the stable promotion of the 0.8.13 prerelease.** It keeps the workflow discovery and async subagent widget fixes from the prerelease while updating Atomic agent guidance.
27
+
28
+ ## Fixes
29
+
30
+ - **Packaged workflow discovery loads reliably.** Package-authored workflow resources load through `jiti`, so SDK imports such as `@bastani/workflows` resolve consistently from the Bun-packaged binary. See [Workflows](/workflows).
31
+ - **Workflow diagnostics are preserved.** Invalid default exports and supported SDK import diagnostics remain visible when workflows are discovered from packaged resources.
32
+ - **Async subagent widget spacing is stable.** Background subagent widgets preserve spacing before the prompt box. See [Subagents](/subagents).
33
+
34
+ </Update>
35
+
36
+ <Update label="May 20, 2026" tags={["v0.8.12"]}>
7
37
 
8
38
  ## New features
9
39
 
10
- - **`/atomic` builtin slash command.** A new onboarding guide is now available from anywhere in Atomic. Run `/atomic` to get oriented, list sections, or jump to a topic by name. See [Usage](/usage).
11
- - **Model reasoning level in the system prompt.** Atomic now passes the active model's reasoning level into the system prompt, so responses better reflect the depth your selected model is configured for. See [Models](/models).
12
- - **Configurable HTTP idle timeout.** You can now tune how long Atomic keeps idle HTTP connections open via settings, which helps on slow or unreliable networks. See [Settings](/settings).
40
+ - **Configurable HTTP idle timeout.** Tune how long Atomic keeps idle HTTP connections open, with presets from 30 seconds through 30 minutes and an option to disable the timeout. See [Settings](/settings).
41
+ - **Workflow documentation and navigation.** Atomic added the Workflows docs page so users can discover built-in workflows, authoring patterns, package setup, and run control from the docs site. See [Workflows](/workflows).
13
42
 
14
43
  ## Updates
15
44
 
16
- - **Smoother subagent rendering.** Live subagent widgets now animate in place without remounting, and result rendering has been refreshed for clearer progress. See [Extensions](/extensions).
17
- - **Consistent duration display.** Elapsed times across bash, subagent, and workflow output are now shown as whole seconds and never go negative.
18
- - **Better Windows process handling.** Process spawning and self-update behavior have been improved on Windows. See [Windows](/windows).
19
- - **Workflow tool rendering is width-aware.** Tool output inside workflow stages now respects the terminal width and forwards builtin tools correctly to stage sessions. See [Workflows](/workflows).
45
+ - **Model reasoning level in the system prompt.** Atomic now includes the active model's reasoning level in generated system prompts and project context markup. See [Models](/models).
46
+ - **Runtime and extension compatibility.** Internal runtime, tool, TUI, tests, and extension example imports moved to explicit `.ts` specifiers for better raw-TypeScript extension compatibility.
47
+ - **Atomic docs refresh.** Package usage, customization, workflows, and release guidance were refreshed for Atomic branding.
20
48
 
21
- ## Bug fixes
49
+ ## Fixes
22
50
 
23
- - **Workflow package imports load reliably.** Package-authored workflows are now loaded through `jiti`, so `@bastani/workflows` imports resolve consistently, including inside the Bun-packaged binary. See [Workflows](/workflows).
24
- - **Binary archives bundle runtime dependencies.** Downloaded `atomic-*` archives now ship with the runtime deps they need, fixing missing-module errors on first run.
25
- - **Workflow text helpers resolve on all platforms.** Fixed a regression where text helpers failed to load in the packaged binary.
26
- - **`/atomic` handles long inputs safely.** Argument parsing for the guide command no longer slows down on adversarial punctuation-heavy input.
27
- - **Settled workflow stages detach from run control.** Completed stages no longer linger in the active run, keeping the workflow view accurate.
51
+ - **Better Windows self-update behavior.** Package-manager installs now handle native dependency cleanup, quarantine cases, and unavailable updates more clearly. See [Windows](/windows).
52
+ - **Smoother subagent rendering.** Live subagent result rendering, async render updates, transient child-error recovery, and live widget animations are more stable. See [Subagents](/subagents).
53
+ - **Workflow stages settle cleanly.** Completed stages detach from run-control tracking, and the empty workflow graph waiting state is centered. See [Workflows](/workflows).
54
+ - **Consistent duration display.** Elapsed times across bash, subagent, and workflow output are shown as whole seconds and never go negative.
28
55
 
29
56
  </Update>
package/docs/docs.json CHANGED
@@ -29,6 +29,7 @@
29
29
  "pages": [
30
30
  "extensions",
31
31
  "skills",
32
+ "subagents",
32
33
  "workflows",
33
34
  "prompt-templates",
34
35
  "themes",