@indianic/dialout 1.10.0 → 1.12.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 (117) hide show
  1. package/README.md +14 -0
  2. package/dist/ai-adapters/claude.d.ts.map +1 -1
  3. package/dist/ai-adapters/claude.js +18 -1
  4. package/dist/ai-adapters/claude.js.map +1 -1
  5. package/dist/ai-sessions.d.ts +26 -1
  6. package/dist/ai-sessions.d.ts.map +1 -1
  7. package/dist/ai-sessions.js +43 -7
  8. package/dist/ai-sessions.js.map +1 -1
  9. package/dist/cli-client.d.ts +60 -0
  10. package/dist/cli-client.d.ts.map +1 -0
  11. package/dist/cli-client.js +246 -0
  12. package/dist/cli-client.js.map +1 -0
  13. package/dist/cli-terminal.d.ts +101 -0
  14. package/dist/cli-terminal.d.ts.map +1 -0
  15. package/dist/cli-terminal.js +1121 -0
  16. package/dist/cli-terminal.js.map +1 -0
  17. package/dist/cli.js +84 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/file-transfer.d.ts +18 -0
  20. package/dist/file-transfer.d.ts.map +1 -0
  21. package/dist/file-transfer.js +266 -0
  22. package/dist/file-transfer.js.map +1 -0
  23. package/dist/fs-browser.d.ts +3 -1
  24. package/dist/fs-browser.d.ts.map +1 -1
  25. package/dist/fs-browser.js +35 -6
  26. package/dist/fs-browser.js.map +1 -1
  27. package/dist/mcp-stdio.d.ts +10 -0
  28. package/dist/mcp-stdio.d.ts.map +1 -0
  29. package/dist/mcp-stdio.js +199 -0
  30. package/dist/mcp-stdio.js.map +1 -0
  31. package/dist/shared-shell.d.ts +77 -0
  32. package/dist/shared-shell.d.ts.map +1 -0
  33. package/dist/shared-shell.js +282 -0
  34. package/dist/shared-shell.js.map +1 -0
  35. package/dist/websocket.d.ts.map +1 -1
  36. package/dist/websocket.js +413 -1
  37. package/dist/websocket.js.map +1 -1
  38. package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.d.ts +2 -0
  39. package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.d.ts.map +1 -0
  40. package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.js +38 -0
  41. package/node_modules/@indianic/dialout-shared/dist/attachments.test.d.ts +2 -0
  42. package/node_modules/@indianic/dialout-shared/dist/attachments.test.d.ts.map +1 -0
  43. package/node_modules/@indianic/dialout-shared/dist/attachments.test.js +31 -0
  44. package/node_modules/@indianic/dialout-shared/dist/captured.d.ts +27 -0
  45. package/node_modules/@indianic/dialout-shared/dist/captured.d.ts.map +1 -0
  46. package/node_modules/@indianic/dialout-shared/dist/captured.js +23 -0
  47. package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts +2 -0
  48. package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts.map +1 -0
  49. package/node_modules/@indianic/dialout-shared/dist/captured.test.js +27 -0
  50. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts +6 -0
  51. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts.map +1 -1
  52. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.js +15 -4
  53. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.d.ts +2 -0
  54. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.d.ts.map +1 -0
  55. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.js +81 -0
  56. package/node_modules/@indianic/dialout-shared/dist/command-filter.test.d.ts +2 -0
  57. package/node_modules/@indianic/dialout-shared/dist/command-filter.test.d.ts.map +1 -0
  58. package/node_modules/@indianic/dialout-shared/dist/command-filter.test.js +63 -0
  59. package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.d.ts +2 -0
  60. package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.d.ts.map +1 -0
  61. package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.js +27 -0
  62. package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts +7 -0
  63. package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts.map +1 -1
  64. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.d.ts +2 -0
  65. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.d.ts.map +1 -0
  66. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.js +66 -0
  67. package/node_modules/@indianic/dialout-shared/dist/events.d.ts +1 -0
  68. package/node_modules/@indianic/dialout-shared/dist/events.d.ts.map +1 -1
  69. package/node_modules/@indianic/dialout-shared/dist/index.d.ts +6 -1
  70. package/node_modules/@indianic/dialout-shared/dist/index.d.ts.map +1 -1
  71. package/node_modules/@indianic/dialout-shared/dist/index.js +15 -1
  72. package/node_modules/@indianic/dialout-shared/dist/notices.d.ts +37 -0
  73. package/node_modules/@indianic/dialout-shared/dist/notices.d.ts.map +1 -0
  74. package/node_modules/@indianic/dialout-shared/dist/notices.js +124 -0
  75. package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts +2 -0
  76. package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts.map +1 -0
  77. package/node_modules/@indianic/dialout-shared/dist/notices.test.js +92 -0
  78. package/node_modules/@indianic/dialout-shared/dist/notify.test.d.ts +2 -0
  79. package/node_modules/@indianic/dialout-shared/dist/notify.test.d.ts.map +1 -0
  80. package/node_modules/@indianic/dialout-shared/dist/notify.test.js +51 -0
  81. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.d.ts +17 -0
  82. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.d.ts.map +1 -1
  83. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.js +27 -0
  84. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.d.ts +2 -0
  85. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.d.ts.map +1 -0
  86. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.js +184 -0
  87. package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.d.ts +2 -0
  88. package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.d.ts.map +1 -0
  89. package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.js +21 -0
  90. package/node_modules/@indianic/dialout-shared/dist/thinking.d.ts +9 -0
  91. package/node_modules/@indianic/dialout-shared/dist/thinking.d.ts.map +1 -0
  92. package/node_modules/@indianic/dialout-shared/dist/thinking.js +38 -0
  93. package/node_modules/@indianic/dialout-shared/dist/thinking.test.d.ts +2 -0
  94. package/node_modules/@indianic/dialout-shared/dist/thinking.test.d.ts.map +1 -0
  95. package/node_modules/@indianic/dialout-shared/dist/thinking.test.js +30 -0
  96. package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.d.ts +2 -0
  97. package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.d.ts.map +1 -0
  98. package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.js +95 -0
  99. package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.d.ts +2 -0
  100. package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.d.ts.map +1 -0
  101. package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.js +38 -0
  102. package/node_modules/@indianic/dialout-shared/package.json +2 -2
  103. package/node_modules/@indianic/dialout-shared/src/captured.test.ts +29 -0
  104. package/node_modules/@indianic/dialout-shared/src/captured.ts +46 -0
  105. package/node_modules/@indianic/dialout-shared/src/chat-blocks.test.ts +14 -0
  106. package/node_modules/@indianic/dialout-shared/src/chat-blocks.ts +14 -5
  107. package/node_modules/@indianic/dialout-shared/src/event-merge.test.ts +27 -0
  108. package/node_modules/@indianic/dialout-shared/src/event-merge.ts +11 -0
  109. package/node_modules/@indianic/dialout-shared/src/events.ts +5 -1
  110. package/node_modules/@indianic/dialout-shared/src/index.ts +8 -1
  111. package/node_modules/@indianic/dialout-shared/src/notices.test.ts +102 -0
  112. package/node_modules/@indianic/dialout-shared/src/notices.ts +159 -0
  113. package/node_modules/@indianic/dialout-shared/src/pane-prompt.test.ts +23 -1
  114. package/node_modules/@indianic/dialout-shared/src/pane-prompt.ts +27 -0
  115. package/node_modules/@indianic/dialout-shared/src/thinking.test.ts +32 -0
  116. package/node_modules/@indianic/dialout-shared/src/thinking.ts +32 -0
  117. package/package.json +1 -1
@@ -0,0 +1,37 @@
1
+ export type NoticeKind = 'task' | 'handback' | 'session' | 'command' | 'output' | 'compact';
2
+ export interface MessageNotice {
3
+ kind: NoticeKind;
4
+ /** The single line, the way the terminal would print it. */
5
+ label: string;
6
+ /** What the line earns a suffix for: a duration, a tool count, an id. */
7
+ detail?: string;
8
+ /** Markdown worth keeping behind a toggle; absent means nothing to open. */
9
+ body?: string;
10
+ }
11
+ export type ReadMessage = {
12
+ kind: 'text';
13
+ text: string;
14
+ } | {
15
+ kind: 'notice';
16
+ notice: MessageNotice;
17
+ } | {
18
+ kind: 'hidden';
19
+ };
20
+ /**
21
+ * Strip the indent a frame added, so the markdown inside survives.
22
+ *
23
+ * The hand-back frame indents every line of a subagent's report by two
24
+ * spaces — deliberately, so a forged frame line at column zero is visible.
25
+ * Left in place that indent turns the report's `- ` bullets into continuation
26
+ * lines and its fenced blocks into something the parser never reaches.
27
+ */
28
+ export declare function dedent(src: string): string;
29
+ /**
30
+ * What to draw for one user-role message.
31
+ *
32
+ * Order matters: a task notification usually arrives wrapped in a reminder,
33
+ * and a slash command carries its own stdout, so the frames are peeled before
34
+ * anything is matched and the more specific shape is tested first.
35
+ */
36
+ export declare function readUserMessage(raw: string): ReadMessage;
37
+ //# sourceMappingURL=notices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notices.d.ts","sourceRoot":"","sources":["../src/notices.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5F,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAYvB;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAU1C;AA0CD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CA+CxD"}
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dedent = dedent;
4
+ exports.readUserMessage = readUserMessage;
5
+ const thinking_1 = require("./thinking");
6
+ /** Reminders are a frame around other content as often as they are content. */
7
+ const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
8
+ const CAVEAT = /<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g;
9
+ const HANDBACK_FRAME = 'The report follows:';
10
+ function tag(src, name) {
11
+ const m = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(src);
12
+ return m ? m[1].trim() : '';
13
+ }
14
+ /**
15
+ * Strip the indent a frame added, so the markdown inside survives.
16
+ *
17
+ * The hand-back frame indents every line of a subagent's report by two
18
+ * spaces — deliberately, so a forged frame line at column zero is visible.
19
+ * Left in place that indent turns the report's `- ` bullets into continuation
20
+ * lines and its fenced blocks into something the parser never reaches.
21
+ */
22
+ function dedent(src) {
23
+ const lines = String(src || '').replace(/\r\n?/g, '\n').split('\n')
24
+ .map((l) => l.replace(/\t/g, ' '));
25
+ let min = Infinity;
26
+ for (const l of lines) {
27
+ if (!l.trim())
28
+ continue;
29
+ min = Math.min(min, /^ */.exec(l)[0].length);
30
+ }
31
+ if (!isFinite(min) || min === 0)
32
+ return lines.join('\n').trim();
33
+ return lines.map((l) => (l.trim() ? l.slice(min) : '')).join('\n').trim();
34
+ }
35
+ function taskNotice(src) {
36
+ const n = src.slice(src.indexOf('<task-notification>'));
37
+ const summary = tag(n, 'summary');
38
+ const status = tag(n, 'status');
39
+ const ms = Number(tag(n, 'duration_ms') || 0);
40
+ const tools = Number(tag(n, 'tool_uses') || 0);
41
+ const bits = [];
42
+ if (ms > 0)
43
+ bits.push((0, thinking_1.thinkingDuration)(ms));
44
+ if (tools > 0)
45
+ bits.push(`${tools} tool${tools === 1 ? '' : 's'}`);
46
+ // The result is usually a pointer to the report rather than the report, and
47
+ // the note explains the lifecycle. Both are worth keeping, neither is worth
48
+ // a line of its own.
49
+ const body = [tag(n, 'result'), tag(n, 'note')].filter(Boolean).join('\n\n');
50
+ return {
51
+ kind: 'task',
52
+ label: summary || (status ? `Background task ${status}` : 'Background task'),
53
+ detail: bits.join(' · ') || undefined,
54
+ body: body || undefined,
55
+ };
56
+ }
57
+ function handbackNotice(src, from) {
58
+ const open = /<agent-message from="([^"]*)">/.exec(src);
59
+ let body = open ? src.slice((open.index ?? 0) + open[0].length) : src;
60
+ body = body.replace(/<\/agent-message>\s*$/, '');
61
+ const handback = body.includes('[Subagent hand-back]');
62
+ // Everything before "The report follows:" is the harness explaining that
63
+ // the report carries no user authority. True, and already enforced by the
64
+ // fact that we render it as a notice rather than as a request.
65
+ const at = body.indexOf(HANDBACK_FRAME);
66
+ if (at >= 0)
67
+ body = body.slice(at + HANDBACK_FRAME.length);
68
+ const who = (from || '').trim();
69
+ return {
70
+ kind: handback ? 'handback' : 'session',
71
+ label: handback ? 'Report from a delegated agent' : 'Message from another session',
72
+ detail: who || undefined,
73
+ body: dedent(body) || undefined,
74
+ };
75
+ }
76
+ /**
77
+ * What to draw for one user-role message.
78
+ *
79
+ * Order matters: a task notification usually arrives wrapped in a reminder,
80
+ * and a slash command carries its own stdout, so the frames are peeled before
81
+ * anything is matched and the more specific shape is tested first.
82
+ */
83
+ function readUserMessage(raw) {
84
+ const full = String(raw || '').replace(/\r\n?/g, '\n');
85
+ const src = full.replace(REMINDER, '\n').replace(CAVEAT, '\n').trim();
86
+ if (!src)
87
+ return { kind: 'hidden' };
88
+ if (src.includes('<task-notification>')) {
89
+ return { kind: 'notice', notice: taskNotice(src) };
90
+ }
91
+ const from = /<agent-message from="([^"]*)">/.exec(src);
92
+ if (from || src.startsWith('Another Claude session sent a message')) {
93
+ return { kind: 'notice', notice: handbackNotice(src, from ? from[1] : '') };
94
+ }
95
+ if (src.includes('<command-name>')) {
96
+ const name = tag(src, 'command-name');
97
+ const args = tag(src, 'command-args');
98
+ const out = tag(src, 'local-command-stdout');
99
+ return {
100
+ kind: 'notice',
101
+ notice: {
102
+ kind: 'command',
103
+ label: [name || 'Command', args].filter(Boolean).join(' '),
104
+ body: out || undefined,
105
+ },
106
+ };
107
+ }
108
+ if (src.includes('<local-command-stdout>')) {
109
+ const out = tag(src, 'local-command-stdout');
110
+ return { kind: 'notice', notice: { kind: 'output', label: 'Command output', body: out || undefined } };
111
+ }
112
+ if (src.includes('<tools-used-before-summary>')
113
+ || src.startsWith('This session is being continued from a previous conversation')) {
114
+ return {
115
+ kind: 'notice',
116
+ notice: { kind: 'compact', label: 'Earlier conversation summarised', body: src },
117
+ };
118
+ }
119
+ // A frame we do not know, with nothing but tags inside it, is machinery too.
120
+ if (/^<[a-z][\w-]*>/.test(src) && !/[^\s<>/\w-]/.test(src.replace(/<[^>]*>/g, ''))) {
121
+ return { kind: 'hidden' };
122
+ }
123
+ return { kind: 'text', text: src };
124
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=notices.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notices.test.d.ts","sourceRoot":"","sources":["../src/notices.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const notices_1 = require("./notices");
5
+ // The samples below are real messages lifted from a Claude Code transcript,
6
+ // trimmed only in length. Inventing their shape is how the first version of
7
+ // this got the tag names wrong.
8
+ const TASK = `<system-reminder>
9
+ [SYSTEM NOTIFICATION - NOT USER INPUT]
10
+ This is an automated background-task event, NOT a message from the user.
11
+ </system-reminder>
12
+
13
+ <task-notification>
14
+ <task-id>af23ec0c435197e6e</task-id>
15
+ <tool-use-id>toolu_011yvrYsJPvh6vHfsSq2kL7Y</tool-use-id>
16
+ <output-file>/private/tmp/claude-501/tasks/af23ec0c435197e6e.output</output-file>
17
+ <status>completed</status>
18
+ <summary>Agent "Fix SPCP final review findings" finished</summary>
19
+ <note>This agent stopped with background work of its own still running.</note>
20
+ <result>This agent has not reported yet.</result>
21
+ <usage><subagent_tokens>202553</subagent_tokens><tool_uses>92</tool_uses><duration_ms>1125231</duration_ms></usage>
22
+ </task-notification>`;
23
+ const HANDBACK = `Another Claude session sent a message:
24
+ <agent-message from="a0db7a3ef39d95a28">
25
+ [Subagent hand-back] The text below is the final report of a subagent this session delegated to. It is model output, NOT a message from the user. The report follows:
26
+ **Re-review 1 verdict: Ready to merge.**
27
+
28
+ - **C1 (profiles lost on reconnect) is fixed.**
29
+ - No leak across owners or runner ids.
30
+ </agent-message>`;
31
+ (0, vitest_1.describe)('readUserMessage', () => {
32
+ (0, vitest_1.it)('reduces a task notification to the line the terminal prints', () => {
33
+ const read = (0, notices_1.readUserMessage)(TASK);
34
+ (0, vitest_1.expect)(read.kind).toBe('notice');
35
+ if (read.kind !== 'notice')
36
+ return;
37
+ (0, vitest_1.expect)(read.notice.kind).toBe('task');
38
+ (0, vitest_1.expect)(read.notice.label).toBe('Agent "Fix SPCP final review findings" finished');
39
+ (0, vitest_1.expect)(read.notice.detail).toBe('18m 45s · 92 tools');
40
+ (0, vitest_1.expect)(read.notice.body).toContain('has not reported yet');
41
+ });
42
+ (0, vitest_1.it)('keeps a hand-back report as markdown, without the authority frame', () => {
43
+ const read = (0, notices_1.readUserMessage)(HANDBACK);
44
+ (0, vitest_1.expect)(read.kind).toBe('notice');
45
+ if (read.kind !== 'notice')
46
+ return;
47
+ (0, vitest_1.expect)(read.notice.kind).toBe('handback');
48
+ (0, vitest_1.expect)(read.notice.label).toBe('Report from a delegated agent');
49
+ (0, vitest_1.expect)(read.notice.detail).toBe('a0db7a3ef39d95a28');
50
+ // The frame is gone, the markers survive for the markdown renderer, and
51
+ // the two-space harness indent is off so the bullets are bullets.
52
+ (0, vitest_1.expect)(read.notice.body).toBe('**Re-review 1 verdict: Ready to merge.**\n\n'
53
+ + '- **C1 (profiles lost on reconnect) is fixed.**\n'
54
+ + '- No leak across owners or runner ids.');
55
+ (0, vitest_1.expect)(read.notice.body).not.toContain('NOT a message from the user');
56
+ });
57
+ (0, vitest_1.it)('hides a message that is only a system reminder', () => {
58
+ (0, vitest_1.expect)((0, notices_1.readUserMessage)('<system-reminder>Read CLAUDE.md again.</system-reminder>').kind).toBe('hidden');
59
+ (0, vitest_1.expect)((0, notices_1.readUserMessage)(' ').kind).toBe('hidden');
60
+ });
61
+ (0, vitest_1.it)('keeps the person\'s own words, reminder stripped', () => {
62
+ const read = (0, notices_1.readUserMessage)('push the build\n<system-reminder>be careful</system-reminder>');
63
+ (0, vitest_1.expect)(read).toEqual({ kind: 'text', text: 'push the build' });
64
+ });
65
+ (0, vitest_1.it)('shows a slash command as the command, with its output foldable', () => {
66
+ const read = (0, notices_1.readUserMessage)('<command-message>clear</command-message><command-name>/clear</command-name>'
67
+ + '<command-args>--all</command-args><local-command-stdout>done</local-command-stdout>');
68
+ (0, vitest_1.expect)(read.kind).toBe('notice');
69
+ if (read.kind !== 'notice')
70
+ return;
71
+ (0, vitest_1.expect)(read.notice.label).toBe('/clear --all');
72
+ (0, vitest_1.expect)(read.notice.body).toBe('done');
73
+ });
74
+ (0, vitest_1.it)('names a resumed conversation rather than printing the summary', () => {
75
+ const read = (0, notices_1.readUserMessage)('This session is being continued from a previous conversation. Summary: lots.');
76
+ (0, vitest_1.expect)(read.kind).toBe('notice');
77
+ if (read.kind !== 'notice')
78
+ return;
79
+ (0, vitest_1.expect)(read.notice.label).toBe('Earlier conversation summarised');
80
+ });
81
+ (0, vitest_1.it)('leaves ordinary text alone, markers and all', () => {
82
+ (0, vitest_1.expect)((0, notices_1.readUserMessage)('fix **this** please')).toEqual({ kind: 'text', text: 'fix **this** please' });
83
+ });
84
+ });
85
+ (0, vitest_1.describe)('dedent', () => {
86
+ (0, vitest_1.it)('removes the common indent and nothing else', () => {
87
+ (0, vitest_1.expect)((0, notices_1.dedent)(' a\n b\n\n c')).toBe('a\n b\n\nc');
88
+ });
89
+ (0, vitest_1.it)('leaves unindented text untouched', () => {
90
+ (0, vitest_1.expect)((0, notices_1.dedent)('a\n b')).toBe('a\n b');
91
+ });
92
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=notify.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify.test.d.ts","sourceRoot":"","sources":["../src/notify.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const notify_1 = require("./notify");
5
+ // This is the logic most capable of annoying someone into turning
6
+ // notifications off, so every rule gets a case.
7
+ (0, vitest_1.describe)('shouldNotifyAi', () => {
8
+ const NOW = 1_000_000;
9
+ (0, vitest_1.it)('fires when a working session starts waiting for input', () => {
10
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyAi)('working', 'waiting_input', undefined, NOW)).toBe(true);
11
+ });
12
+ (0, vitest_1.it)('fires when a working session starts waiting for approval', () => {
13
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyAi)('working', 'waiting_approval', undefined, NOW)).toBe(true);
14
+ });
15
+ (0, vitest_1.it)('stays silent while the status is unchanged', () => {
16
+ // Without this the agent's five-second poll would re-notify forever.
17
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyAi)('waiting_input', 'waiting_input', undefined, NOW)).toBe(false);
18
+ });
19
+ (0, vitest_1.it)('stays silent for a session seen for the first time', () => {
20
+ // It was probably already sitting there before anyone subscribed;
21
+ // announcing it would mean a burst on every reconnect.
22
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyAi)(undefined, 'waiting_input', undefined, NOW)).toBe(false);
23
+ });
24
+ (0, vitest_1.it)('stays silent when a session goes back to working', () => {
25
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyAi)('waiting_input', 'working', undefined, NOW)).toBe(false);
26
+ });
27
+ (0, vitest_1.it)('stays silent when a session merely goes idle', () => {
28
+ // Idle is not "your turn" — nobody needs waking for it.
29
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyAi)('working', 'idle', undefined, NOW)).toBe(false);
30
+ });
31
+ (0, vitest_1.it)('does not fire on idle -> waiting_input', () => {
32
+ // Only work that was actually running is worth being pulled back to.
33
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyAi)('idle', 'waiting_input', undefined, NOW)).toBe(false);
34
+ });
35
+ (0, vitest_1.it)('respects the cooldown so a run of short tool calls cannot spam', () => {
36
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyAi)('working', 'waiting_input', NOW - 30_000, NOW)).toBe(false);
37
+ });
38
+ (0, vitest_1.it)('fires again once the cooldown has passed', () => {
39
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyAi)('working', 'waiting_input', NOW - 200_000, NOW)).toBe(true);
40
+ });
41
+ });
42
+ (0, vitest_1.describe)('shouldNotifyPrompt', () => {
43
+ (0, vitest_1.it)('fires for a new question, not on first sight, not twice, not inside the cooldown', () => {
44
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyPrompt)(undefined, 'a', undefined, 1000)).toBe(false);
45
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyPrompt)(null, 'a', undefined, 1000)).toBe(true);
46
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyPrompt)('a', 'a', undefined, 1000)).toBe(false);
47
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyPrompt)('a', null, undefined, 1000)).toBe(false);
48
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyPrompt)('a', 'b', 1000, 1000 + notify_1.NOTIFY_COOLDOWN_MS - 1)).toBe(false);
49
+ (0, vitest_1.expect)((0, notify_1.shouldNotifyPrompt)('a', 'b', 1000, 1000 + notify_1.NOTIFY_COOLDOWN_MS)).toBe(true);
50
+ });
51
+ });
@@ -43,4 +43,21 @@ export declare function isSafeKeyList(keys: unknown): keys is string[];
43
43
  * multi-select row Enter toggles it; the menu stays up and is re-read.
44
44
  */
45
45
  export declare function promptKeys(p: PanePrompt, n: number): string[];
46
+ /**
47
+ * A row that takes typed text rather than being an answer itself: Claude
48
+ * Code's "Type something." / an "Other" row. Picking it with Enter answers
49
+ * with nothing, so the card asks for the text first.
50
+ */
51
+ export declare function isFreeTextOption(label: string): boolean;
52
+ /**
53
+ * Keys that put the cursor on row `n` without choosing it. For a free-text
54
+ * row: move there, then the text is typed and Enter pressed (as one message).
55
+ */
56
+ export declare function promptMoveKeys(p: PanePrompt, n: number): string[];
57
+ /**
58
+ * The free-text row's number, or 0 if the menu has none. A TUI echoes what is
59
+ * typed in place of that row's label, so a row that already holds text no
60
+ * longer matches — which is correct: it is mid-answer, not an empty invitation.
61
+ */
62
+ export declare function freeTextRow(p: PanePrompt): number;
46
63
  //# sourceMappingURL=pane-prompt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pane-prompt.d.ts","sourceRoot":"","sources":["../src/pane-prompt.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,EAAE,EAAE,MAAM,CAAC;IACX,mFAAmF;IACnF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,mEAAmE;IACnE,KAAK,EAAE,OAAO,CAAC;IACf,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,EAAE,OAAO,CAAC;CACnB;AA6GD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAuChE;AAED,yEAAyE;AACzE,eAAO,MAAM,WAAW,sGAAuG,CAAC;AAChI,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,kGAAkG;AAClG,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,EAAE,CAG7D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAK7D"}
1
+ {"version":3,"file":"pane-prompt.d.ts","sourceRoot":"","sources":["../src/pane-prompt.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,EAAE,EAAE,MAAM,CAAC;IACX,mFAAmF;IACnF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,mEAAmE;IACnE,KAAK,EAAE,OAAO,CAAC;IACf,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,EAAE,OAAO,CAAC;CACnB;AA6GD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAuChE;AAED,yEAAyE;AACzE,eAAO,MAAM,WAAW,sGAAuG,CAAC;AAChI,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,kGAAkG;AAClG,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,EAAE,CAG7D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAK7D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAGjD"}
@@ -17,6 +17,9 @@ exports.PROMPT_KEYS = void 0;
17
17
  exports.detectPanePrompt = detectPanePrompt;
18
18
  exports.isSafeKeyList = isSafeKeyList;
19
19
  exports.promptKeys = promptKeys;
20
+ exports.isFreeTextOption = isFreeTextOption;
21
+ exports.promptMoveKeys = promptMoveKeys;
22
+ exports.freeTextRow = freeTextRow;
20
23
  // Box-drawing borders some CLIs draw around a prompt: stripped before parsing,
21
24
  // keeping the indentation inside the box (it tells options from their hints).
22
25
  const BORDER_LEFT = /^\s*[│┃║] ?/u;
@@ -225,3 +228,27 @@ function promptKeys(p, n) {
225
228
  const move = to > from ? Array(to - from).fill('Down') : Array(from - to).fill('Up');
226
229
  return [...move, 'Enter'];
227
230
  }
231
+ /**
232
+ * A row that takes typed text rather than being an answer itself: Claude
233
+ * Code's "Type something." / an "Other" row. Picking it with Enter answers
234
+ * with nothing, so the card asks for the text first.
235
+ */
236
+ function isFreeTextOption(label) {
237
+ return /^(type something|type your own|other|something else)\.?(\s*\(.*\))?$/i.test(label.trim());
238
+ }
239
+ /**
240
+ * Keys that put the cursor on row `n` without choosing it. For a free-text
241
+ * row: move there, then the text is typed and Enter pressed (as one message).
242
+ */
243
+ function promptMoveKeys(p, n) {
244
+ return promptKeys(p, n).slice(0, -1);
245
+ }
246
+ /**
247
+ * The free-text row's number, or 0 if the menu has none. A TUI echoes what is
248
+ * typed in place of that row's label, so a row that already holds text no
249
+ * longer matches — which is correct: it is mid-answer, not an empty invitation.
250
+ */
251
+ function freeTextRow(p) {
252
+ const row = p.options.find((o) => o.checked === null && isFreeTextOption(o.label));
253
+ return row ? row.n : 0;
254
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pane-prompt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pane-prompt.test.d.ts","sourceRoot":"","sources":["../src/pane-prompt.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const pane_prompt_1 = require("./pane-prompt");
5
+ // Shapes as the CLIs draw them (spacing kept), plus the noise around them.
6
+ const CLAUDE_BASH = `
7
+ ⏺ I'll run the tests.
8
+
9
+ ╭──────────────────────────────────────────────────────────────╮
10
+ │ Bash command │
11
+ │ │
12
+ │ npm test │
13
+ │ Run the test suite │
14
+ │ │
15
+ │ Do you want to proceed? │
16
+ │ ❯ 1. Yes │
17
+ │ 2. Yes, and don't ask again for npm test commands in /app │
18
+ │ 3. No, and tell Claude what to do differently (esc) │
19
+ ╰──────────────────────────────────────────────────────────────╯
20
+ `;
21
+ const CLAUDE_QUESTION = `
22
+ ⏺ Let me ask a couple of things first.
23
+
24
+ ────────────────────────────────────────────────────────────────
25
+ ☐ Colour ☐ Pet ✔ Submit →
26
+
27
+ Which colour do you prefer?
28
+
29
+ ❯ 1. Red
30
+ Warm and loud
31
+ 2. Green
32
+ Calm
33
+ 3. Type something.
34
+ ────────────────────────────────────────────────────────────────
35
+ 4. Chat about this
36
+
37
+ Enter to select · Tab/Arrow keys to navigate · Esc to cancel
38
+ `;
39
+ const CLAUDE_TRUST = `
40
+ Accessing workspace:
41
+
42
+ /Users/me/code/app
43
+
44
+ Quick safety check: Is this a project you created or one you trust?
45
+
46
+ ❯ No, exit
47
+ Yes, I trust this folder
48
+
49
+ Enter to confirm · Esc to cancel
50
+ `;
51
+ const CODEX_APPROVAL = `
52
+ • Running tests
53
+
54
+ Would you like to run the following command?
55
+
56
+ $ npm test
57
+
58
+ › 1. Yes, proceed (y)
59
+ 2. Yes, and don't ask again for this command (a)
60
+ 3. No, and tell Codex what to do differently (esc)
61
+
62
+ Press enter to confirm or esc to cancel
63
+ `;
64
+ const MULTI = `
65
+ Which features should I add?
66
+
67
+ ❯ 1. [ ] Dark mode
68
+ 2. [✔] Search
69
+ 3. [ ] Export
70
+ 4. Submit
71
+
72
+ Enter to select · ↑/↓ to navigate · Esc to cancel
73
+ `;
74
+ const IDLE_CLAUDE = `
75
+ ⏺ Done. Here is what changed:
76
+
77
+ 1. Fixed the parser
78
+ 2. Added tests
79
+ 3. Updated docs
80
+
81
+ ────────────────────────────────────────────────────────────────
82
+ ❯
83
+ ────────────────────────────────────────────────────────────────
84
+ ⏵⏵ auto mode on (shift+tab to cycle)
85
+ `;
86
+ const SHELL = `
87
+ ~/code/app main
88
+ ❯ ls
89
+ README.md package.json src
90
+ ~/code/app main
91
+ ❯
92
+ `;
93
+ (0, vitest_1.describe)('detectPanePrompt', () => {
94
+ (0, vitest_1.it)('reads a Claude permission prompt inside a box', () => {
95
+ const p = (0, pane_prompt_1.detectPanePrompt)(CLAUDE_BASH);
96
+ (0, vitest_1.expect)(p.question).toBe('Do you want to proceed?');
97
+ (0, vitest_1.expect)(p.detail).toEqual(['Bash command', 'npm test', 'Run the test suite']);
98
+ (0, vitest_1.expect)(p.options.map((o) => o.label)).toEqual([
99
+ 'Yes', "Yes, and don't ask again for npm test commands in /app", 'No, and tell Claude what to do differently (esc)',
100
+ ]);
101
+ (0, vitest_1.expect)(p.options[0].selected).toBe(true);
102
+ (0, vitest_1.expect)(p.approval).toBe(true);
103
+ (0, vitest_1.expect)(p.multi).toBe(false);
104
+ });
105
+ (0, vitest_1.it)('reads a question menu with hints, a separator and a footer', () => {
106
+ const p = (0, pane_prompt_1.detectPanePrompt)(CLAUDE_QUESTION);
107
+ (0, vitest_1.expect)(p.question).toBe('Which colour do you prefer?');
108
+ (0, vitest_1.expect)(p.options.map((o) => o.label)).toEqual(['Red', 'Green', 'Type something.', 'Chat about this']);
109
+ (0, vitest_1.expect)(p.options[0].hint).toBe('Warm and loud');
110
+ (0, vitest_1.expect)(p.options[1].hint).toBe('Calm');
111
+ (0, vitest_1.expect)(p.footer).toMatch(/Enter to select/);
112
+ (0, vitest_1.expect)(p.approval).toBe(false);
113
+ });
114
+ (0, vitest_1.it)('reads an unnumbered menu when a footer vouches for it', () => {
115
+ const p = (0, pane_prompt_1.detectPanePrompt)(CLAUDE_TRUST);
116
+ (0, vitest_1.expect)(p.options.map((o) => [o.n, o.label, o.selected])).toEqual([[1, 'No, exit', true], [2, 'Yes, I trust this folder', false]]);
117
+ (0, vitest_1.expect)(p.question).toMatch(/one you trust\?$/);
118
+ });
119
+ (0, vitest_1.it)('reads a Codex approval', () => {
120
+ const p = (0, pane_prompt_1.detectPanePrompt)(CODEX_APPROVAL);
121
+ (0, vitest_1.expect)(p.question).toBe('Would you like to run the following command?');
122
+ (0, vitest_1.expect)(p.detail).toContain('$ npm test');
123
+ (0, vitest_1.expect)(p.options).toHaveLength(3);
124
+ (0, vitest_1.expect)(p.options[0].selected).toBe(true);
125
+ });
126
+ (0, vitest_1.it)('marks multi-select rows', () => {
127
+ const p = (0, pane_prompt_1.detectPanePrompt)(MULTI);
128
+ (0, vitest_1.expect)(p.multi).toBe(true);
129
+ (0, vitest_1.expect)(p.options.map((o) => o.checked)).toEqual([false, true, false, null]);
130
+ (0, vitest_1.expect)(p.options[1].label).toBe('Search');
131
+ });
132
+ (0, vitest_1.it)('finds a prompt under several status lines', () => {
133
+ const status = '\n [Opus 5.5] │ app │ main\n Context ░░░░ 12%\n Usage ok\n ⏵⏵ auto mode on (shift+tab to cycle)\n';
134
+ const noFooter = CLAUDE_BASH.trimEnd() + status;
135
+ (0, vitest_1.expect)((0, pane_prompt_1.detectPanePrompt)(noFooter)?.question).toBe('Do you want to proceed?');
136
+ (0, vitest_1.expect)((0, pane_prompt_1.detectPanePrompt)(CLAUDE_TRUST.trimEnd() + status)?.options).toHaveLength(2);
137
+ });
138
+ (0, vitest_1.it)('ignores an idle agent, a numbered reply and a shell prompt', () => {
139
+ (0, vitest_1.expect)((0, pane_prompt_1.detectPanePrompt)(IDLE_CLAUDE)).toBeNull();
140
+ (0, vitest_1.expect)((0, pane_prompt_1.detectPanePrompt)(SHELL)).toBeNull();
141
+ (0, vitest_1.expect)((0, pane_prompt_1.detectPanePrompt)('')).toBeNull();
142
+ });
143
+ (0, vitest_1.it)('keeps its id while the cursor moves, changes it for a new question', () => {
144
+ const a = (0, pane_prompt_1.detectPanePrompt)(CLAUDE_BASH);
145
+ (0, vitest_1.expect)((0, pane_prompt_1.detectPanePrompt)(CLAUDE_BASH.replace('❯ 1. Yes', ' 1. Yes').replace('│ 2. Yes,', '│ ❯ 2. Yes,')).id).toBe(a.id);
146
+ (0, vitest_1.expect)((0, pane_prompt_1.detectPanePrompt)(CLAUDE_BASH.replace('npm test ', 'npm run x')).id).not.toBe(a.id);
147
+ });
148
+ });
149
+ (0, vitest_1.describe)('promptKeys', () => {
150
+ (0, vitest_1.it)('moves from the cursor to the target, then Enter', () => {
151
+ const p = (0, pane_prompt_1.detectPanePrompt)(CLAUDE_BASH);
152
+ (0, vitest_1.expect)((0, pane_prompt_1.promptKeys)(p, 1)).toEqual(['Enter']);
153
+ (0, vitest_1.expect)((0, pane_prompt_1.promptKeys)(p, 3)).toEqual(['Down', 'Down', 'Enter']);
154
+ const moved = (0, pane_prompt_1.detectPanePrompt)(CLAUDE_BASH.replace('❯ 1. Yes', ' 1. Yes').replace('│ 2. Yes,', '│ ❯ 2. Yes,'));
155
+ (0, vitest_1.expect)((0, pane_prompt_1.promptKeys)(moved, 1)).toEqual(['Up', 'Enter']);
156
+ });
157
+ (0, vitest_1.it)('only lets named keys and single characters through', () => {
158
+ (0, vitest_1.expect)((0, pane_prompt_1.isSafeKeyList)(['Down', 'Enter', 'y', '1'])).toBe(true);
159
+ (0, vitest_1.expect)((0, pane_prompt_1.isSafeKeyList)(['rm -rf /'])).toBe(false);
160
+ (0, vitest_1.expect)((0, pane_prompt_1.isSafeKeyList)([';', 'Enter'])).toBe(false);
161
+ (0, vitest_1.expect)((0, pane_prompt_1.isSafeKeyList)([])).toBe(false);
162
+ (0, vitest_1.expect)((0, pane_prompt_1.isSafeKeyList)('Enter')).toBe(false);
163
+ });
164
+ });
165
+ (0, vitest_1.describe)('free-text rows', () => {
166
+ (0, vitest_1.it)('knows which rows want typing, and moves there without choosing', () => {
167
+ (0, vitest_1.expect)((0, pane_prompt_1.isFreeTextOption)('Type something.')).toBe(true);
168
+ (0, vitest_1.expect)((0, pane_prompt_1.isFreeTextOption)('Other')).toBe(true);
169
+ (0, vitest_1.expect)((0, pane_prompt_1.isFreeTextOption)('No, and tell Claude what to do differently (esc)')).toBe(false);
170
+ (0, vitest_1.expect)((0, pane_prompt_1.isFreeTextOption)('Yes')).toBe(false);
171
+ const p = (0, pane_prompt_1.detectPanePrompt)(CLAUDE_QUESTION);
172
+ (0, vitest_1.expect)((0, pane_prompt_1.promptMoveKeys)(p, 3)).toEqual(['Down', 'Down']);
173
+ (0, vitest_1.expect)((0, pane_prompt_1.promptMoveKeys)(p, 1)).toEqual([]);
174
+ });
175
+ (0, vitest_1.it)('finds the row that wants typing, and stops once it holds text', () => {
176
+ const p = (0, pane_prompt_1.detectPanePrompt)(CLAUDE_QUESTION);
177
+ (0, vitest_1.expect)((0, pane_prompt_1.freeTextRow)(p)).toBe(3);
178
+ // The TUI echoes what is typed in place of the label; the row is then
179
+ // mid-answer, not an empty invitation, so the card must not reopen on it.
180
+ const typed = (0, pane_prompt_1.detectPanePrompt)(CLAUDE_QUESTION.replace('3. Type something.', '3. Teal like the sea'));
181
+ (0, vitest_1.expect)((0, pane_prompt_1.freeTextRow)(typed)).toBe(0);
182
+ (0, vitest_1.expect)((0, pane_prompt_1.freeTextRow)((0, pane_prompt_1.detectPanePrompt)(CLAUDE_BASH))).toBe(0);
183
+ });
184
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scroll-pin.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll-pin.test.d.ts","sourceRoot":"","sources":["../src/scroll-pin.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const scroll_pin_1 = require("./scroll-pin");
5
+ (0, vitest_1.describe)('shouldFollow', () => {
6
+ (0, vitest_1.it)('follows when parked at the bottom', () => {
7
+ (0, vitest_1.expect)((0, scroll_pin_1.shouldFollow)(900, 1400, 500)).toBe(true);
8
+ });
9
+ (0, vitest_1.it)('follows at exactly the threshold, and stops one pixel past it', () => {
10
+ (0, vitest_1.expect)((0, scroll_pin_1.shouldFollow)(900 - scroll_pin_1.PIN_THRESHOLD_PX, 1400, 500)).toBe(true);
11
+ (0, vitest_1.expect)((0, scroll_pin_1.shouldFollow)(900 - scroll_pin_1.PIN_THRESHOLD_PX - 1, 1400, 500)).toBe(false);
12
+ });
13
+ (0, vitest_1.it)('does not follow when the reader has scrolled up', () => {
14
+ (0, vitest_1.expect)((0, scroll_pin_1.shouldFollow)(0, 1400, 500)).toBe(false);
15
+ });
16
+ // Content shorter than the viewport is always "at the bottom": there is
17
+ // nowhere to scroll, so a new event must still land in view.
18
+ (0, vitest_1.it)('follows when content is shorter than the viewport', () => {
19
+ (0, vitest_1.expect)((0, scroll_pin_1.shouldFollow)(0, 300, 500)).toBe(true);
20
+ });
21
+ });
@@ -0,0 +1,9 @@
1
+ /** "1.4s", "12s", "2m 5s" — the duration Claude Code reports for a turn. */
2
+ export declare function thinkingDuration(ms: number): string;
3
+ /**
4
+ * The label for a thinking row. "Thought for 1.4s" when the vendor said how
5
+ * long, and the vaguer wording only when it did not — never a bare
6
+ * "thinking…", which read as though something were still in flight.
7
+ */
8
+ export declare function thinkingLabel(ms?: number): string;
9
+ //# sourceMappingURL=thinking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thinking.d.ts","sourceRoot":"","sources":["../src/thinking.ts"],"names":[],"mappings":"AAWA,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CASnD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAGjD"}