@bastani/atomic 0.8.14 → 0.8.15

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 (53) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +0 -8
  3. package/dist/builtin/intercom/CHANGELOG.md +5 -0
  4. package/dist/builtin/intercom/package.json +1 -1
  5. package/dist/builtin/mcp/CHANGELOG.md +5 -0
  6. package/dist/builtin/mcp/index.ts +4 -8
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +2 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/subagents/skills/tmux/SKILL.md +220 -0
  11. package/dist/builtin/subagents/skills/tmux/scripts/find-sessions.sh +112 -0
  12. package/dist/builtin/subagents/skills/tmux/scripts/wait-for-text.sh +83 -0
  13. package/dist/builtin/web-access/CHANGELOG.md +5 -0
  14. package/dist/builtin/web-access/package.json +1 -1
  15. package/dist/builtin/workflows/CHANGELOG.md +12 -1
  16. package/dist/builtin/workflows/README.md +3 -1
  17. package/dist/builtin/workflows/builtin/ralph.ts +222 -295
  18. package/dist/builtin/workflows/package.json +1 -1
  19. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +20 -11
  20. package/dist/builtin/workflows/src/extension/index.ts +1 -0
  21. package/dist/builtin/workflows/src/extension/status-writer.ts +18 -3
  22. package/dist/builtin/workflows/src/runs/background/runner.ts +8 -10
  23. package/dist/builtin/workflows/src/runs/foreground/executor.ts +484 -91
  24. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +13 -2
  25. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +41 -15
  26. package/dist/builtin/workflows/src/runs/shared/graph-inference.ts +31 -0
  27. package/dist/builtin/workflows/src/runs/shared/prompt-callsite.ts +98 -0
  28. package/dist/builtin/workflows/src/shared/persistence-restore.ts +3 -1
  29. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
  30. package/dist/builtin/workflows/src/shared/store-types.ts +12 -1
  31. package/dist/builtin/workflows/src/shared/store.ts +77 -3
  32. package/dist/builtin/workflows/src/tui/graph-view.ts +17 -1
  33. package/dist/builtin/workflows/src/tui/prompt-card.ts +185 -30
  34. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +386 -21
  35. package/docs/changelog.mdx +41 -14
  36. package/docs/docs.json +1 -0
  37. package/docs/extensions.md +19 -19
  38. package/docs/images/workflow-input-picker.png +0 -0
  39. package/docs/images/workflow-list.png +0 -0
  40. package/docs/index.md +33 -27
  41. package/docs/providers.md +2 -2
  42. package/docs/quickstart.md +15 -15
  43. package/docs/sdk.md +8 -8
  44. package/docs/sessions.md +5 -5
  45. package/docs/settings.md +27 -1
  46. package/docs/skills.md +2 -2
  47. package/docs/subagents.md +157 -0
  48. package/docs/usage.md +7 -7
  49. package/docs/windows.md +8 -0
  50. package/docs/workflows.md +62 -9
  51. package/package.json +2 -1
  52. package/docs/images/doom-extension.png +0 -0
  53. package/docs/images/exy.png +0 -3
@@ -37,7 +37,17 @@ import {
37
37
  import type { PendingPrompt } from "../shared/store-types.js";
38
38
  import type { GraphTheme } from "./graph-theme.js";
39
39
  import { hexToAnsi, hexBg, paint, RESET, BOLD } from "./color-utils.js";
40
- import { Key, matchesKey } from "./text-helpers.js";
40
+ import { Key, decodePrintableKey, matchesKey } from "./text-helpers.js";
41
+ import {
42
+ type KeybindingsLike,
43
+ TUI_ACTION,
44
+ deleteRange,
45
+ lineEnd,
46
+ lineStart,
47
+ matchesAction,
48
+ wordLeft,
49
+ wordRight,
50
+ } from "./keybindings-adapter.js";
41
51
 
42
52
  // ---------------------------------------------------------------------------
43
53
  // State
@@ -98,6 +108,7 @@ export type PromptCardAction =
98
108
  export function handlePromptCardInput(
99
109
  data: string,
100
110
  state: PromptCardState,
111
+ keybindings?: KeybindingsLike,
101
112
  ): PromptCardAction {
102
113
  if (matchesKey(data, Key.ctrl("c")) || matchesKey(data, Key.escape)) {
103
114
  return { kind: "cancel" };
@@ -109,9 +120,9 @@ export function handlePromptCardInput(
109
120
  case "select":
110
121
  return handleSelect(data, state);
111
122
  case "input":
112
- return handleInput(data, state);
123
+ return handleInput(data, state, keybindings);
113
124
  case "editor":
114
- return handleEditor(data, state);
125
+ return handleEditor(data, state, keybindings);
115
126
  }
116
127
  }
117
128
 
@@ -154,56 +165,138 @@ function handleSelect(data: string, state: PromptCardState): PromptCardAction {
154
165
  return action;
155
166
  }
156
167
 
157
- function handleInput(data: string, state: PromptCardState): PromptCardAction {
158
- if (matchesKey(data, Key.enter)) {
168
+ function handleInput(
169
+ data: string,
170
+ state: PromptCardState,
171
+ keybindings: KeybindingsLike | undefined,
172
+ ): PromptCardAction {
173
+ if (matchesTextAction(keybindings, data, TUI_ACTION.inputSubmit, Key.enter)) {
159
174
  return { kind: "submit", response: state.rawText };
160
175
  }
161
- return applyTextEdit(data, state);
176
+ if (matchesAction(keybindings, data, "tui.input.newLine")) {
177
+ insertText(state, "\n");
178
+ return { kind: "noop" };
179
+ }
180
+ return applyTextEdit(data, state, keybindings, { multiline: true });
162
181
  }
163
182
 
164
- function handleEditor(data: string, state: PromptCardState): PromptCardAction {
183
+ function handleEditor(
184
+ data: string,
185
+ state: PromptCardState,
186
+ keybindings: KeybindingsLike | undefined,
187
+ ): PromptCardAction {
165
188
  if (matchesKey(data, Key.tab) || matchesKey(data, Key.shift("tab"))) {
166
189
  state.editorSubmitFocused = !state.editorSubmitFocused;
167
190
  return { kind: "noop" };
168
191
  }
169
192
  if (state.editorSubmitFocused) {
170
- if (matchesKey(data, Key.enter)) {
193
+ if (matchesTextAction(keybindings, data, TUI_ACTION.inputSubmit, Key.enter)) {
171
194
  return { kind: "submit", response: state.rawText };
172
195
  }
173
196
  return { kind: "noop" };
174
197
  }
175
- if (matchesKey(data, Key.enter)) {
176
- state.rawText = state.rawText.slice(0, state.caret) + "\n" + state.rawText.slice(state.caret);
177
- state.caret += 1;
198
+ if (
199
+ matchesTextAction(keybindings, data, TUI_ACTION.inputSubmit, Key.enter) ||
200
+ matchesAction(keybindings, data, "tui.input.newLine")
201
+ ) {
202
+ insertText(state, "\n");
178
203
  return { kind: "noop" };
179
204
  }
180
- return applyTextEdit(data, state);
205
+ return applyTextEdit(data, state, keybindings, { multiline: true });
181
206
  }
182
207
 
183
208
  function applyTextEdit(
184
209
  data: string,
185
210
  state: PromptCardState,
211
+ keybindings: KeybindingsLike | undefined,
212
+ opts: { multiline: boolean },
186
213
  ): PromptCardAction {
187
- if (matchesKey(data, Key.left)) {
188
- state.caret = previousGraphemeBoundary(state.rawText, state.caret);
214
+ const caret = Math.max(0, Math.min(state.caret, state.rawText.length));
215
+ state.caret = caret;
216
+
217
+ if (opts.multiline && matchesAction(keybindings, data, TUI_ACTION.editorCursorUp)) {
218
+ const nextCaret = caretLineUp(state.rawText, caret);
219
+ if (nextCaret !== null) state.caret = nextCaret;
220
+ return { kind: "noop" };
221
+ }
222
+ if (opts.multiline && matchesAction(keybindings, data, TUI_ACTION.editorCursorDown)) {
223
+ const nextCaret = caretLineDown(state.rawText, caret);
224
+ if (nextCaret !== null) state.caret = nextCaret;
225
+ return { kind: "noop" };
226
+ }
227
+ if (matchesAction(keybindings, data, "tui.editor.cursorWordLeft")) {
228
+ state.caret = wordLeft(state.rawText, caret);
229
+ return { kind: "noop" };
230
+ }
231
+ if (matchesAction(keybindings, data, "tui.editor.cursorWordRight")) {
232
+ state.caret = wordRight(state.rawText, caret);
233
+ return { kind: "noop" };
234
+ }
235
+ if (matchesAction(keybindings, data, "tui.editor.cursorLineStart")) {
236
+ state.caret = lineStart(state.rawText, caret);
237
+ return { kind: "noop" };
238
+ }
239
+ if (matchesAction(keybindings, data, "tui.editor.cursorLineEnd")) {
240
+ state.caret = lineEnd(state.rawText, caret);
241
+ return { kind: "noop" };
242
+ }
243
+ if (matchesTextAction(keybindings, data, TUI_ACTION.editorCursorLeft, Key.left)) {
244
+ state.caret = previousGraphemeBoundary(state.rawText, caret);
245
+ return { kind: "noop" };
246
+ }
247
+ if (matchesTextAction(keybindings, data, TUI_ACTION.editorCursorRight, Key.right)) {
248
+ state.caret = nextGraphemeBoundary(state.rawText, caret);
249
+ return { kind: "noop" };
250
+ }
251
+ if (matchesAction(keybindings, data, "tui.editor.deleteWordBackward")) {
252
+ const start = wordLeft(state.rawText, caret);
253
+ const result = deleteRange(state.rawText, start, caret, caret);
254
+ state.rawText = result.text;
255
+ state.caret = result.caret;
189
256
  return { kind: "noop" };
190
257
  }
191
- if (matchesKey(data, Key.right)) {
192
- state.caret = nextGraphemeBoundary(state.rawText, state.caret);
258
+ if (matchesAction(keybindings, data, "tui.editor.deleteWordForward")) {
259
+ const end = wordRight(state.rawText, caret);
260
+ const result = deleteRange(state.rawText, caret, end, caret);
261
+ state.rawText = result.text;
262
+ state.caret = result.caret;
193
263
  return { kind: "noop" };
194
264
  }
195
- if (matchesKey(data, Key.backspace)) {
196
- if (state.caret > 0) {
197
- const prev = previousGraphemeBoundary(state.rawText, state.caret);
198
- state.rawText = state.rawText.slice(0, prev) + state.rawText.slice(state.caret);
199
- state.caret = prev;
265
+ if (matchesAction(keybindings, data, "tui.editor.deleteToLineStart")) {
266
+ const start = lineStart(state.rawText, caret);
267
+ const result = deleteRange(state.rawText, start, caret, caret);
268
+ state.rawText = result.text;
269
+ state.caret = result.caret;
270
+ return { kind: "noop" };
271
+ }
272
+ if (matchesAction(keybindings, data, "tui.editor.deleteToLineEnd")) {
273
+ const end = lineEnd(state.rawText, caret);
274
+ const result = deleteRange(state.rawText, caret, end, caret);
275
+ state.rawText = result.text;
276
+ state.caret = result.caret;
277
+ return { kind: "noop" };
278
+ }
279
+ if (matchesTextAction(keybindings, data, "tui.editor.deleteCharBackward", Key.backspace)) {
280
+ if (caret > 0) {
281
+ const prev = previousGraphemeBoundary(state.rawText, caret);
282
+ const result = deleteRange(state.rawText, prev, caret, caret);
283
+ state.rawText = result.text;
284
+ state.caret = result.caret;
285
+ }
286
+ return { kind: "noop" };
287
+ }
288
+ if (matchesAction(keybindings, data, "tui.editor.deleteCharForward")) {
289
+ if (caret < state.rawText.length) {
290
+ const result = deleteRange(state.rawText, caret, nextGraphemeBoundary(state.rawText, caret), caret);
291
+ state.rawText = result.text;
292
+ state.caret = result.caret;
200
293
  }
201
294
  return { kind: "noop" };
202
295
  }
203
- if (isPrintableText(data)) {
204
- state.rawText =
205
- state.rawText.slice(0, state.caret) + data + state.rawText.slice(state.caret);
206
- state.caret += data.length;
296
+
297
+ const printable = decodePrintableKey(data) ?? data;
298
+ if (isPrintableText(printable)) {
299
+ insertText(state, printable);
207
300
  return { kind: "noop" };
208
301
  }
209
302
  return { kind: "noop" };
@@ -250,10 +343,73 @@ function nextGraphemeBoundary(value: string, caret: number): number {
250
343
  return value.length;
251
344
  }
252
345
 
346
+ function clampGraphemeBoundary(value: string, caret: number): number {
347
+ const safeCaret = Math.max(0, Math.min(caret, value.length));
348
+ if (safeCaret === value.length) return safeCaret;
349
+ for (const part of graphemeParts(value)) {
350
+ if (part.start === safeCaret) return safeCaret;
351
+ if (part.start > safeCaret) break;
352
+ }
353
+ return previousGraphemeBoundary(value, safeCaret);
354
+ }
355
+
253
356
  function isPrintableText(data: string): boolean {
254
357
  return data.length > 0 && !data.startsWith("\x1b") && !/[\x00-\x1f\x7f]/.test(data);
255
358
  }
256
359
 
360
+ function insertText(state: PromptCardState, text: string): void {
361
+ const caret = Math.max(0, Math.min(state.caret, state.rawText.length));
362
+ state.rawText = state.rawText.slice(0, caret) + text + state.rawText.slice(caret);
363
+ state.caret = caret + text.length;
364
+ }
365
+
366
+ function matchesTextAction(
367
+ keybindings: KeybindingsLike | undefined,
368
+ data: string,
369
+ action: string,
370
+ fallback?: Parameters<typeof matchesKey>[1],
371
+ ): boolean {
372
+ return matchesAction(keybindings, data, action) || (fallback !== undefined && matchesKey(data, fallback));
373
+ }
374
+
375
+ function visualColumnAt(text: string, caret: number): number {
376
+ return visibleWidth(text.slice(0, clampGraphemeBoundary(text, caret)));
377
+ }
378
+
379
+ function offsetAtVisualColumn(text: string, targetCol: number): number {
380
+ let col = 0;
381
+ for (const part of graphemeParts(text)) {
382
+ const width = part.width;
383
+ if (col + width > targetCol) return part.start;
384
+ col += width;
385
+ }
386
+ return text.length;
387
+ }
388
+
389
+ function caretLineUp(raw: string, caret: number): number | null {
390
+ const safe = clampGraphemeBoundary(raw, caret);
391
+ const lineStartOffset = raw.lastIndexOf("\n", safe - 1) + 1;
392
+ if (lineStartOffset === 0) return null;
393
+ const prevLineEnd = lineStartOffset - 1;
394
+ const prevLineStart = raw.lastIndexOf("\n", prevLineEnd - 1) + 1;
395
+ const col = visualColumnAt(raw.slice(lineStartOffset, safe), raw.slice(lineStartOffset, safe).length);
396
+ const prevLine = raw.slice(prevLineStart, prevLineEnd);
397
+ return prevLineStart + offsetAtVisualColumn(prevLine, col);
398
+ }
399
+
400
+ function caretLineDown(raw: string, caret: number): number | null {
401
+ const safe = clampGraphemeBoundary(raw, caret);
402
+ const nextNl = raw.indexOf("\n", safe);
403
+ if (nextNl === -1) return null;
404
+ const lineStartOffset = raw.lastIndexOf("\n", safe - 1) + 1;
405
+ const col = visualColumnAt(raw.slice(lineStartOffset, safe), raw.slice(lineStartOffset, safe).length);
406
+ const nextLineStart = nextNl + 1;
407
+ const nextNlAfter = raw.indexOf("\n", nextLineStart);
408
+ const nextLineEnd = nextNlAfter === -1 ? raw.length : nextNlAfter;
409
+ const nextLine = raw.slice(nextLineStart, nextLineEnd);
410
+ return nextLineStart + offsetAtVisualColumn(nextLine, col);
411
+ }
412
+
257
413
  /**
258
414
  * Compute the safe default response when the user dismisses the prompt.
259
415
  * Used by the overlay to keep the workflow body unblocked even on cancel.
@@ -358,9 +514,8 @@ export interface PromptCardRenderOpts {
358
514
  export function renderPromptCard(opts: PromptCardRenderOpts): string[] {
359
515
  const { state, theme, width } = opts;
360
516
  const innerWidth = Math.max(20, width - 2);
361
- const accent = theme.accent;
362
- const borderColor = accent;
363
- const bg = hexBg(theme.backgroundPanel);
517
+ const borderColor = theme.border;
518
+ const bg = "";
364
519
 
365
520
  const lines: string[] = [];
366
521
  lines.push(makeBorderTop(borderColor, " AWAITING INPUT ", theme, innerWidth, bg));
@@ -390,9 +545,9 @@ function makeBorderTop(
390
545
  innerWidth: number,
391
546
  bg: string,
392
547
  ): string {
393
- const labelText = paint(label, theme.text, { bold: true });
548
+ const labelText = paint(label, theme.textMuted, { bold: true });
394
549
  const labelW = visibleWidth(labelText);
395
- const fillLen = Math.max(0, innerWidth - labelW - 2);
550
+ const fillLen = Math.max(0, innerWidth - labelW);
396
551
  return (
397
552
  bg +
398
553
  paint("╭", color) +