@agents-ensemble/cli 0.13.0 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/config.example.yaml +3 -3
- package/dist/format-session-summary.d.ts.map +1 -1
- package/dist/format-session-summary.js +51 -45
- package/dist/format-session-summary.js.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/session-log-lines.js +1 -1
- package/dist/session-log-lines.js.map +1 -1
- package/dist/tui/activity-log.d.ts +1 -1
- package/dist/tui/activity-log.d.ts.map +1 -1
- package/dist/tui/activity-log.js +16 -2
- package/dist/tui/activity-log.js.map +1 -1
- package/dist/tui/create-issue-session-tui-host.d.ts.map +1 -1
- package/dist/tui/create-issue-session-tui-host.js +1 -1
- package/dist/tui/create-issue-session-tui-host.js.map +1 -1
- package/dist/tui/format-operator-context.d.ts.map +1 -1
- package/dist/tui/format-operator-context.js +3 -3
- package/dist/tui/format-operator-context.js.map +1 -1
- package/dist/tui/ink-test-keys.d.ts +4 -0
- package/dist/tui/ink-test-keys.d.ts.map +1 -1
- package/dist/tui/ink-test-keys.js +4 -0
- package/dist/tui/ink-test-keys.js.map +1 -1
- package/dist/tui/issue-session-tui-stream-cursor.test.js +3 -3
- package/dist/tui/issue-session-tui-stream-cursor.test.js.map +1 -1
- package/dist/tui/issue-session-tui-stream.d.ts.map +1 -1
- package/dist/tui/issue-session-tui-stream.js +133 -15
- package/dist/tui/issue-session-tui-stream.js.map +1 -1
- package/dist/tui/issue-session-tui-stream.test.js +369 -13
- package/dist/tui/issue-session-tui-stream.test.js.map +1 -1
- package/dist/tui/issue-session-tui.d.ts +3 -2
- package/dist/tui/issue-session-tui.d.ts.map +1 -1
- package/dist/tui/issue-session-tui.js +205 -21
- package/dist/tui/issue-session-tui.js.map +1 -1
- package/dist/tui/issue-session-tui.test.js +553 -37
- package/dist/tui/issue-session-tui.test.js.map +1 -1
- package/dist/tui/open-question-answer-suggestions.d.ts +19 -0
- package/dist/tui/open-question-answer-suggestions.d.ts.map +1 -0
- package/dist/tui/open-question-answer-suggestions.js +62 -0
- package/dist/tui/open-question-answer-suggestions.js.map +1 -0
- package/dist/tui/open-questions-pane.d.ts +19 -0
- package/dist/tui/open-questions-pane.d.ts.map +1 -1
- package/dist/tui/open-questions-pane.js +35 -34
- package/dist/tui/open-questions-pane.js.map +1 -1
- package/dist/tui/operator-command-suggestions.d.ts +15 -0
- package/dist/tui/operator-command-suggestions.d.ts.map +1 -1
- package/dist/tui/operator-input-history.d.ts +13 -0
- package/dist/tui/operator-input-history.d.ts.map +1 -0
- package/dist/tui/operator-input-history.js +51 -0
- package/dist/tui/operator-input-history.js.map +1 -0
- package/dist/tui/operator-input-layout.d.ts +2 -0
- package/dist/tui/operator-input-layout.d.ts.map +1 -1
- package/dist/tui/operator-input-layout.js +73 -0
- package/dist/tui/operator-input-layout.js.map +1 -1
- package/dist/tui/operator-text-area.d.ts +6 -4
- package/dist/tui/operator-text-area.d.ts.map +1 -1
- package/dist/tui/operator-text-area.js +17 -5
- package/dist/tui/operator-text-area.js.map +1 -1
- package/dist/tui/operator-text-area.test-double.d.ts +1 -1
- package/dist/tui/operator-text-area.test-double.d.ts.map +1 -1
- package/dist/tui/operator-text-area.test-double.js +15 -14
- package/dist/tui/operator-text-area.test-double.js.map +1 -1
- package/dist/tui/operator-text-area.test.js +17 -3
- package/dist/tui/operator-text-area.test.js.map +1 -1
- package/dist/tui/pane-layout.d.ts +2 -0
- package/dist/tui/pane-layout.d.ts.map +1 -1
- package/dist/tui/pane-layout.js +7 -3
- package/dist/tui/pane-layout.js.map +1 -1
- package/dist/tui/stream-layout.d.ts +3 -3
- package/dist/tui/stream-layout.d.ts.map +1 -1
- package/dist/tui/stream-layout.js +6 -6
- package/dist/tui/stream-layout.js.map +1 -1
- package/dist/tui/tui-layout-constants.d.ts +4 -2
- package/dist/tui/tui-layout-constants.d.ts.map +1 -1
- package/dist/tui/tui-layout-constants.js +4 -2
- package/dist/tui/tui-layout-constants.js.map +1 -1
- package/dist/tui/tui-view-model.d.ts.map +1 -1
- package/dist/tui/tui-view-model.js +2 -8
- package/dist/tui/tui-view-model.js.map +1 -1
- package/dist/tui/worker-display-name.d.ts +2 -0
- package/dist/tui/worker-display-name.d.ts.map +1 -0
- package/dist/tui/worker-display-name.js +4 -0
- package/dist/tui/worker-display-name.js.map +1 -0
- package/docs/config.md +92 -16
- package/docs/settings.md +3 -3
- package/package.json +3 -3
|
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
vi.mock('./operator-text-area.js', () => import('./operator-text-area.test-double.js'));
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import { cleanup, render } from 'ink-testing-library';
|
|
6
|
+
import stringWidth from 'string-width';
|
|
6
7
|
import { IssueSessionTui } from './issue-session-tui.js';
|
|
7
8
|
import { createTuiViewModel } from './tui-view-model.js';
|
|
8
9
|
import { buildActivityLogDisplayLines } from './activity-log.js';
|
|
@@ -10,10 +11,11 @@ import { resolveOperatorInputDisplayMode } from './format-operator-context.js';
|
|
|
10
11
|
import { flushInkStdin, INK_TEST_KEYS } from './ink-test-keys.js';
|
|
11
12
|
import { extractOrchestrationPaneFrameStats, extractTuiPaneFrameStats } from './orchestration-pane-frame.js';
|
|
12
13
|
import { resolveOpenQuestionsPaneLayout } from './open-questions-pane.js';
|
|
14
|
+
import { computeWorkerPaneHeight } from './pane-layout.js';
|
|
13
15
|
import { computeActivityLogLineCapacity, computeActivityPaneHeight, computeInputPaneHeight, computeOrchestrationLogVisibleLineCount, computeOperatorInputCursorX, computeOperatorInputLineIndex, } from './compute-operator-input-cursor-y.js';
|
|
14
16
|
import { getPaneContentWidth } from './wrap-text-to-width.js';
|
|
15
17
|
import { resolvePaneHeights } from './pane-layout.js';
|
|
16
|
-
import { INPUT_PANE_TITLE, MAIN_PANE_TITLE, OPEN_QUESTIONS_PANE_MIN_HEIGHT, OPERATOR_INPUT_CURSOR_Y_OFFSET, PANE_PADDING_X, ROUND_BORDER_WIDTH,
|
|
18
|
+
import { INPUT_PANE_TITLE, MAIN_PANE_TITLE, OPEN_QUESTIONS_PANE_MIN_HEIGHT, OPERATOR_INPUT_PROMPT, OPERATOR_INPUT_CURSOR_Y_OFFSET, PANE_PADDING_X, ROUND_BORDER_WIDTH, WORKER_PANE_TITLE, } from './tui-layout-constants.js';
|
|
17
19
|
import { createTuiResizeController, createTuiTerminalSizeStore, TUI_RESIZE_SETTLE_MS, TUI_RESIZE_SHRINK_COALESCE_MS, } from './tui-terminal-size.js';
|
|
18
20
|
class ResizeSource extends EventEmitter {
|
|
19
21
|
columns = 80;
|
|
@@ -21,12 +23,12 @@ class ResizeSource extends EventEmitter {
|
|
|
21
23
|
}
|
|
22
24
|
function findOperatorInputLine(lines) {
|
|
23
25
|
const operatorLineIndices = lines
|
|
24
|
-
.map((line, index) => (line.includes(
|
|
26
|
+
.map((line, index) => (line.includes(OPERATOR_INPUT_PROMPT) ? index : -1))
|
|
25
27
|
.filter((index) => index >= 0);
|
|
26
28
|
const lineIndex = operatorLineIndices.at(-1) ?? -1;
|
|
27
29
|
const line = lines[lineIndex] ?? '';
|
|
28
|
-
const promptIndex = line.lastIndexOf(
|
|
29
|
-
const inputStartX = promptIndex >= 0 ? promptIndex +
|
|
30
|
+
const promptIndex = line.lastIndexOf(OPERATOR_INPUT_PROMPT);
|
|
31
|
+
const inputStartX = promptIndex >= 0 ? promptIndex + OPERATOR_INPUT_PROMPT.length : -1;
|
|
30
32
|
return { lineIndex, inputStartX };
|
|
31
33
|
}
|
|
32
34
|
function expectNoContentOnBorderLines(frame) {
|
|
@@ -34,6 +36,7 @@ function expectNoContentOnBorderLines(frame) {
|
|
|
34
36
|
if (line.startsWith('╭') || line.startsWith('╰') || line.startsWith('┌') || line.startsWith('└')) {
|
|
35
37
|
expect(line).not.toMatch(/\[(?:operator|conductor|implementer|reviewer)\]/);
|
|
36
38
|
expect(line).not.toMatch(/operator>/);
|
|
39
|
+
expect(line).not.toContain(OPERATOR_INPUT_PROMPT);
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -52,6 +55,7 @@ function fillScrollableHarnessLog(viewModel, count = 30) {
|
|
|
52
55
|
const SCROLL_HINT = 'PgUp/PgDn でスクロール';
|
|
53
56
|
const ISSUE_URL = 'https://github.com/otolab/agents-ensemble/issues/249';
|
|
54
57
|
const NON_CANONICAL_ISSUE_URL = 'http://github.com/otolab/agents-ensemble/issues/249/';
|
|
58
|
+
const LONG_QUESTION_CONTEXT = Array.from({ length: 40 }, (_, index) => `context-line-${String(index).padStart(2, '0')} ${'wrapped segment '.repeat(6)}`).join('\n');
|
|
55
59
|
function createOpenQuestion(overrides) {
|
|
56
60
|
return {
|
|
57
61
|
responseType: 'text',
|
|
@@ -96,17 +100,180 @@ describe('IssueSessionTui', () => {
|
|
|
96
100
|
stdin.write('/');
|
|
97
101
|
await flushInkStdin();
|
|
98
102
|
const allCommandsFrame = lastFrame() ?? '';
|
|
103
|
+
expect(allCommandsFrame).toContain(`${OPERATOR_INPUT_PROMPT}/`);
|
|
104
|
+
expect(allCommandsFrame).not.toContain('operator> ');
|
|
99
105
|
expect(allCommandsFrame).toContain('/reconnect — conductor を再接続');
|
|
100
106
|
expect(allCommandsFrame).toContain('/exit — セッションを終了');
|
|
101
107
|
stdin.write('ex');
|
|
102
108
|
await flushInkStdin();
|
|
103
109
|
const filteredFrame = lastFrame() ?? '';
|
|
110
|
+
expect(filteredFrame).toContain(`${OPERATOR_INPUT_PROMPT}/ex`);
|
|
104
111
|
expect(filteredFrame).toContain('/exit — セッションを終了');
|
|
105
112
|
expect(filteredFrame).not.toContain('/reconnect — conductor を再接続');
|
|
106
113
|
stdin.write('\r');
|
|
107
114
|
await flushInkStdin();
|
|
108
115
|
expect(onSubmit).toHaveBeenCalledWith('/ex', undefined);
|
|
109
116
|
});
|
|
117
|
+
it('navigates submitted history at visual boundaries and restores an edited draft', async () => {
|
|
118
|
+
const viewModel = createTuiViewModel();
|
|
119
|
+
const onSubmit = vi.fn();
|
|
120
|
+
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: onSubmit }));
|
|
121
|
+
stdin.write('A');
|
|
122
|
+
await flushInkStdin();
|
|
123
|
+
stdin.write('\r');
|
|
124
|
+
await flushInkStdin();
|
|
125
|
+
expect(onSubmit).toHaveBeenCalledWith('A', undefined);
|
|
126
|
+
stdin.write('\u001B[200~line1\nline2\u001B[201~');
|
|
127
|
+
await flushInkStdin();
|
|
128
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
129
|
+
await flushInkStdin();
|
|
130
|
+
stdin.write('X');
|
|
131
|
+
await flushInkStdin();
|
|
132
|
+
stdin.write(INK_TEST_KEYS.home);
|
|
133
|
+
await flushInkStdin();
|
|
134
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
135
|
+
await flushInkStdin();
|
|
136
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}A`);
|
|
137
|
+
stdin.write("'");
|
|
138
|
+
await flushInkStdin();
|
|
139
|
+
stdin.write('\r');
|
|
140
|
+
await flushInkStdin();
|
|
141
|
+
expect(onSubmit).toHaveBeenLastCalledWith("A'", undefined);
|
|
142
|
+
stdin.write(INK_TEST_KEYS.downArrow);
|
|
143
|
+
await flushInkStdin();
|
|
144
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}line1X`);
|
|
145
|
+
expect(lastFrame() ?? '').toContain('line2');
|
|
146
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
147
|
+
await flushInkStdin();
|
|
148
|
+
stdin.write(INK_TEST_KEYS.home);
|
|
149
|
+
await flushInkStdin();
|
|
150
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
151
|
+
await flushInkStdin();
|
|
152
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}A'`);
|
|
153
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
154
|
+
await flushInkStdin();
|
|
155
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}A`);
|
|
156
|
+
});
|
|
157
|
+
it('keeps an Open Question answer completion after a historical submit', async () => {
|
|
158
|
+
const viewModel = createTuiViewModel();
|
|
159
|
+
const question = createOpenQuestion({
|
|
160
|
+
id: 'inq-yes-no',
|
|
161
|
+
question: 'Proceed?',
|
|
162
|
+
responseType: 'yes_no',
|
|
163
|
+
});
|
|
164
|
+
viewModel.setDisplayState({
|
|
165
|
+
workers: {},
|
|
166
|
+
conductorOutput: null,
|
|
167
|
+
openQuestions: [question],
|
|
168
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
169
|
+
});
|
|
170
|
+
const onSubmit = vi.fn();
|
|
171
|
+
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: onSubmit }));
|
|
172
|
+
stdin.write('previous input');
|
|
173
|
+
await flushInkStdin();
|
|
174
|
+
stdin.write('\r');
|
|
175
|
+
await flushInkStdin();
|
|
176
|
+
stdin.write('unsent draft');
|
|
177
|
+
await flushInkStdin();
|
|
178
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
179
|
+
await flushInkStdin();
|
|
180
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}previous input`);
|
|
181
|
+
stdin.write(' edited');
|
|
182
|
+
await flushInkStdin();
|
|
183
|
+
stdin.write('\r');
|
|
184
|
+
await flushInkStdin();
|
|
185
|
+
expect(onSubmit).toHaveBeenLastCalledWith('previous input edited', {
|
|
186
|
+
targetOpenQuestionId: question.id,
|
|
187
|
+
});
|
|
188
|
+
stdin.write('\r');
|
|
189
|
+
await flushInkStdin();
|
|
190
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}yes`);
|
|
191
|
+
stdin.write(INK_TEST_KEYS.downArrow);
|
|
192
|
+
await flushInkStdin();
|
|
193
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}yes`);
|
|
194
|
+
expect(lastFrame() ?? '').not.toContain(`${OPERATOR_INPUT_PROMPT}unsent draft`);
|
|
195
|
+
stdin.write('\r');
|
|
196
|
+
await flushInkStdin();
|
|
197
|
+
expect(onSubmit).toHaveBeenLastCalledWith('yes', {
|
|
198
|
+
targetOpenQuestionId: question.id,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
it('moves within a soft-wrapped input and enters history only at its visual boundaries', async () => {
|
|
202
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
203
|
+
configurable: true,
|
|
204
|
+
value: 44,
|
|
205
|
+
});
|
|
206
|
+
const viewModel = createTuiViewModel();
|
|
207
|
+
const onSubmit = vi.fn();
|
|
208
|
+
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: onSubmit }));
|
|
209
|
+
const olderEntry = 'b'.repeat(90);
|
|
210
|
+
stdin.write(olderEntry);
|
|
211
|
+
await flushInkStdin();
|
|
212
|
+
stdin.write('\r');
|
|
213
|
+
await flushInkStdin();
|
|
214
|
+
const wrappedDraft = 'a'.repeat(90);
|
|
215
|
+
stdin.write(wrappedDraft);
|
|
216
|
+
await flushInkStdin();
|
|
217
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
218
|
+
await flushInkStdin();
|
|
219
|
+
stdin.write('X');
|
|
220
|
+
await flushInkStdin();
|
|
221
|
+
stdin.write(INK_TEST_KEYS.downArrow);
|
|
222
|
+
await flushInkStdin();
|
|
223
|
+
stdin.write('Y');
|
|
224
|
+
await flushInkStdin();
|
|
225
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
226
|
+
await flushInkStdin();
|
|
227
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
228
|
+
await flushInkStdin();
|
|
229
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
230
|
+
await flushInkStdin();
|
|
231
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}${olderEntry.slice(0, 10)}`);
|
|
232
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
233
|
+
await flushInkStdin();
|
|
234
|
+
stdin.write(INK_TEST_KEYS.downArrow);
|
|
235
|
+
await flushInkStdin();
|
|
236
|
+
stdin.write(INK_TEST_KEYS.downArrow);
|
|
237
|
+
await flushInkStdin();
|
|
238
|
+
stdin.write('\r');
|
|
239
|
+
await flushInkStdin();
|
|
240
|
+
const editedWrappedDraft = onSubmit.mock.calls.at(-1)?.[0];
|
|
241
|
+
expect(editedWrappedDraft).toHaveLength(wrappedDraft.length + 2);
|
|
242
|
+
expect(editedWrappedDraft.indexOf('Y') - editedWrappedDraft.indexOf('X')).toBeGreaterThan(1);
|
|
243
|
+
expect(editedWrappedDraft.replace(/[XY]/g, '')).toBe(wrappedDraft);
|
|
244
|
+
stdin.write('pending draft');
|
|
245
|
+
await flushInkStdin();
|
|
246
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
247
|
+
await flushInkStdin();
|
|
248
|
+
stdin.write(INK_TEST_KEYS.downArrow);
|
|
249
|
+
await flushInkStdin();
|
|
250
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}pending draft`);
|
|
251
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
252
|
+
await flushInkStdin();
|
|
253
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
254
|
+
await flushInkStdin();
|
|
255
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
256
|
+
await flushInkStdin();
|
|
257
|
+
stdin.write(INK_TEST_KEYS.upArrow);
|
|
258
|
+
await flushInkStdin();
|
|
259
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}${olderEntry.slice(0, 10)}`);
|
|
260
|
+
stdin.write('\r');
|
|
261
|
+
await flushInkStdin();
|
|
262
|
+
expect(onSubmit).toHaveBeenLastCalledWith(olderEntry, undefined);
|
|
263
|
+
});
|
|
264
|
+
it('sizes the input pane using the shortened prompt width', async () => {
|
|
265
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
266
|
+
configurable: true,
|
|
267
|
+
value: 20,
|
|
268
|
+
});
|
|
269
|
+
const viewModel = createTuiViewModel();
|
|
270
|
+
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
271
|
+
stdin.write('1234567890');
|
|
272
|
+
await flushInkStdin();
|
|
273
|
+
const frame = lastFrame() ?? '';
|
|
274
|
+
expect(frame).toContain(`${OPERATOR_INPUT_PROMPT}1234567890`);
|
|
275
|
+
expect(frame).not.toContain('operator> ');
|
|
276
|
+
});
|
|
110
277
|
it('selects slash suggestions with bounded arrows and completes with Tab before Enter submits', async () => {
|
|
111
278
|
const viewModel = createTuiViewModel();
|
|
112
279
|
const onSubmit = vi.fn();
|
|
@@ -124,12 +291,12 @@ describe('IssueSessionTui', () => {
|
|
|
124
291
|
expect(lastFrame() ?? '').toContain('› /close');
|
|
125
292
|
stdin.write(INK_TEST_KEYS.downArrow);
|
|
126
293
|
await flushInkStdin();
|
|
127
|
-
expect(lastFrame() ?? '').toContain('› /
|
|
294
|
+
expect(lastFrame() ?? '').toContain('› /idle');
|
|
128
295
|
for (const key of [INK_TEST_KEYS.upArrow, INK_TEST_KEYS.upArrow]) {
|
|
129
296
|
stdin.write(key);
|
|
130
297
|
await flushInkStdin();
|
|
131
298
|
}
|
|
132
|
-
expect(lastFrame() ?? '').toContain('› /
|
|
299
|
+
expect(lastFrame() ?? '').toContain('› /exit');
|
|
133
300
|
stdin.write(INK_TEST_KEYS.upArrow);
|
|
134
301
|
await flushInkStdin();
|
|
135
302
|
expect(lastFrame() ?? '').toContain('› /reconnect');
|
|
@@ -141,12 +308,107 @@ describe('IssueSessionTui', () => {
|
|
|
141
308
|
stdin.write(key);
|
|
142
309
|
await flushInkStdin();
|
|
143
310
|
}
|
|
144
|
-
expect(lastFrame() ?? '').toContain(
|
|
311
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}/close`);
|
|
312
|
+
expect(lastFrame() ?? '').not.toContain('operator> ');
|
|
145
313
|
expect(onSubmit).not.toHaveBeenCalled();
|
|
146
314
|
stdin.write('\r');
|
|
147
315
|
await flushInkStdin();
|
|
148
316
|
expect(onSubmit).toHaveBeenCalledWith('/close', undefined);
|
|
149
317
|
});
|
|
318
|
+
it.each([
|
|
319
|
+
{
|
|
320
|
+
name: 'choice',
|
|
321
|
+
question: createOpenQuestion({
|
|
322
|
+
id: 'inq-choice',
|
|
323
|
+
question: 'Choose a mode',
|
|
324
|
+
responseType: 'choice',
|
|
325
|
+
choices: [
|
|
326
|
+
{ id: 'safe', label: 'Safe mode' },
|
|
327
|
+
{ id: 'secure', label: 'Secure mode' },
|
|
328
|
+
{ id: 'fast', label: 'Fast mode' },
|
|
329
|
+
],
|
|
330
|
+
}),
|
|
331
|
+
prefix: 's',
|
|
332
|
+
completedValue: 'secure',
|
|
333
|
+
expectedSuggestion: 'safe: Safe mode',
|
|
334
|
+
hasMultipleSuggestions: true,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'yes_no',
|
|
338
|
+
question: createOpenQuestion({
|
|
339
|
+
id: 'inq-yes-no',
|
|
340
|
+
question: 'Proceed?',
|
|
341
|
+
responseType: 'yes_no',
|
|
342
|
+
}),
|
|
343
|
+
prefix: 'n',
|
|
344
|
+
completedValue: 'no',
|
|
345
|
+
expectedSuggestion: 'no — いいえ',
|
|
346
|
+
hasMultipleSuggestions: false,
|
|
347
|
+
},
|
|
348
|
+
])('completes and submits the selected $name answer in pane mode', async ({ question, prefix, completedValue, expectedSuggestion, hasMultipleSuggestions, }) => {
|
|
349
|
+
const viewModel = createTuiViewModel();
|
|
350
|
+
viewModel.setDisplayState({
|
|
351
|
+
workers: {},
|
|
352
|
+
conductorOutput: null,
|
|
353
|
+
openQuestions: [
|
|
354
|
+
question,
|
|
355
|
+
createOpenQuestion({
|
|
356
|
+
id: 'inq-text',
|
|
357
|
+
question: 'Anything else?',
|
|
358
|
+
}),
|
|
359
|
+
],
|
|
360
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
361
|
+
});
|
|
362
|
+
const onSubmit = vi.fn();
|
|
363
|
+
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: onSubmit }));
|
|
364
|
+
stdin.write(prefix);
|
|
365
|
+
await flushInkStdin();
|
|
366
|
+
expect(lastFrame() ?? '').toContain(expectedSuggestion);
|
|
367
|
+
stdin.write(INK_TEST_KEYS.shiftDownArrow);
|
|
368
|
+
await flushInkStdin();
|
|
369
|
+
expect(lastFrame() ?? '').toContain('2/2');
|
|
370
|
+
expect(lastFrame() ?? '').not.toContain(`› ${expectedSuggestion}`);
|
|
371
|
+
stdin.write(INK_TEST_KEYS.shiftUpArrow);
|
|
372
|
+
await flushInkStdin();
|
|
373
|
+
expect(lastFrame() ?? '').toContain('1/2');
|
|
374
|
+
expect(lastFrame() ?? '').toContain(expectedSuggestion);
|
|
375
|
+
if (hasMultipleSuggestions) {
|
|
376
|
+
stdin.write(INK_TEST_KEYS.downArrow);
|
|
377
|
+
await flushInkStdin();
|
|
378
|
+
expect(lastFrame() ?? '').toContain('› secure: Secure mode');
|
|
379
|
+
}
|
|
380
|
+
stdin.write('\r');
|
|
381
|
+
await flushInkStdin();
|
|
382
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
383
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}${completedValue}`);
|
|
384
|
+
expect(lastFrame() ?? '').not.toContain(`› ${completedValue}:`);
|
|
385
|
+
stdin.write(' because it is safer');
|
|
386
|
+
await flushInkStdin();
|
|
387
|
+
stdin.write('\r');
|
|
388
|
+
await flushInkStdin();
|
|
389
|
+
expect(onSubmit).toHaveBeenCalledWith(`${completedValue} because it is safer`, { targetOpenQuestionId: question.id });
|
|
390
|
+
});
|
|
391
|
+
it('prioritizes slash suggestions over answer suggestions in pane mode', async () => {
|
|
392
|
+
const viewModel = createTuiViewModel();
|
|
393
|
+
viewModel.setDisplayState({
|
|
394
|
+
workers: {},
|
|
395
|
+
conductorOutput: null,
|
|
396
|
+
openQuestions: [
|
|
397
|
+
createOpenQuestion({
|
|
398
|
+
id: 'inq-choice',
|
|
399
|
+
question: 'Choose a mode',
|
|
400
|
+
responseType: 'choice',
|
|
401
|
+
choices: [{ id: 'exit', label: 'Exit mode' }],
|
|
402
|
+
}),
|
|
403
|
+
],
|
|
404
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
405
|
+
});
|
|
406
|
+
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
407
|
+
stdin.write('/');
|
|
408
|
+
await flushInkStdin();
|
|
409
|
+
expect(lastFrame() ?? '').toContain('› /reconnect');
|
|
410
|
+
expect(lastFrame() ?? '').not.toContain('› exit: Exit mode');
|
|
411
|
+
});
|
|
150
412
|
it('resets selection to the first matching candidate when the prefix changes', async () => {
|
|
151
413
|
const viewModel = createTuiViewModel();
|
|
152
414
|
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
@@ -162,7 +424,7 @@ describe('IssueSessionTui', () => {
|
|
|
162
424
|
expect(lastFrame() ?? '').toContain('› /exit');
|
|
163
425
|
stdin.write(INK_TEST_KEYS.tab);
|
|
164
426
|
await flushInkStdin();
|
|
165
|
-
expect(lastFrame() ?? '').toContain(
|
|
427
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}/exit`);
|
|
166
428
|
});
|
|
167
429
|
it('keeps Tab and arrow editing behavior when there is no match or input is multiline', async () => {
|
|
168
430
|
const viewModel = createTuiViewModel();
|
|
@@ -172,7 +434,7 @@ describe('IssueSessionTui', () => {
|
|
|
172
434
|
await flushInkStdin();
|
|
173
435
|
stdin.write(INK_TEST_KEYS.tab);
|
|
174
436
|
await flushInkStdin();
|
|
175
|
-
expect(lastFrame() ?? '').toContain(
|
|
437
|
+
expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}/unknown`);
|
|
176
438
|
expect(onSubmit).not.toHaveBeenCalled();
|
|
177
439
|
stdin.write('\r');
|
|
178
440
|
await flushInkStdin();
|
|
@@ -225,7 +487,7 @@ describe('IssueSessionTui', () => {
|
|
|
225
487
|
expect(frame).toContain('inq-1');
|
|
226
488
|
expect(frame).toContain('1/1');
|
|
227
489
|
expect(frame).toContain('Shift+↑↓で選択');
|
|
228
|
-
expect(frame).toContain(
|
|
490
|
+
expect(frame).toContain(OPERATOR_INPUT_PROMPT);
|
|
229
491
|
expect(frame).toContain(INPUT_PANE_TITLE);
|
|
230
492
|
});
|
|
231
493
|
it('shows dispatch hold count in the Workers pane title', () => {
|
|
@@ -255,7 +517,7 @@ describe('IssueSessionTui', () => {
|
|
|
255
517
|
viewModel.setPostLoopWaiting(true);
|
|
256
518
|
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
257
519
|
const frame = lastFrame() ?? '';
|
|
258
|
-
expect(frame).toContain(
|
|
520
|
+
expect(frame).toContain(OPERATOR_INPUT_PROMPT);
|
|
259
521
|
expect(frame).not.toContain('追加指示');
|
|
260
522
|
expect(frame).not.toContain('/reconnect');
|
|
261
523
|
expect(frame).not.toContain('post-loop 待機中');
|
|
@@ -276,7 +538,7 @@ describe('IssueSessionTui', () => {
|
|
|
276
538
|
expect(frame).toContain(issueLabel);
|
|
277
539
|
expect(frame).toContain(osc8Open);
|
|
278
540
|
expect(frame).not.toContain(nonCanonicalOsc8Open);
|
|
279
|
-
expect(frame).toContain(
|
|
541
|
+
expect(frame).toContain(OPERATOR_INPUT_PROMPT);
|
|
280
542
|
expect(frame).not.toContain('任意入力');
|
|
281
543
|
expect(frame).not.toContain('/reconnect');
|
|
282
544
|
viewModel.setDisplayState({
|
|
@@ -313,7 +575,7 @@ describe('IssueSessionTui', () => {
|
|
|
313
575
|
expect(lastFrame() ?? '').toContain(issueLabel);
|
|
314
576
|
expect(lastFrame() ?? '').toContain(osc8Open);
|
|
315
577
|
expect(lastFrame() ?? '').not.toContain(nonCanonicalOsc8Open);
|
|
316
|
-
expect(lastFrame() ?? '').toContain(
|
|
578
|
+
expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
|
|
317
579
|
expect(lastFrame() ?? '').not.toContain('追加指示');
|
|
318
580
|
expect(lastFrame() ?? '').not.toContain('/reconnect');
|
|
319
581
|
expect(lastFrame() ?? '').not.toContain('post-loop 待機中');
|
|
@@ -407,7 +669,7 @@ describe('IssueSessionTui', () => {
|
|
|
407
669
|
await flushInkStdin();
|
|
408
670
|
expectCanonicalIssueUrl();
|
|
409
671
|
const postLoopFrame = (lastFrame() ?? '').replace(/\s+/g, ' ');
|
|
410
|
-
expect(postLoopFrame).toContain(
|
|
672
|
+
expect(postLoopFrame).toContain(OPERATOR_INPUT_PROMPT);
|
|
411
673
|
expect(postLoopFrame).not.toContain('追加指示');
|
|
412
674
|
expect(postLoopFrame).not.toContain('/reconnect');
|
|
413
675
|
viewModel.setShuttingDown(true);
|
|
@@ -488,7 +750,7 @@ describe('IssueSessionTui', () => {
|
|
|
488
750
|
maxTurns: null,
|
|
489
751
|
openQuestions: [],
|
|
490
752
|
});
|
|
491
|
-
const operatorPrompt =
|
|
753
|
+
const operatorPrompt = OPERATOR_INPUT_PROMPT;
|
|
492
754
|
const hintLineCount = resolveOperatorInputDisplayMode({
|
|
493
755
|
openQuestions: viewModel.getSnapshot().operatorContext?.openQuestions ?? [],
|
|
494
756
|
}).hintLines.length;
|
|
@@ -529,7 +791,7 @@ describe('IssueSessionTui', () => {
|
|
|
529
791
|
maxTurns: null,
|
|
530
792
|
openQuestions: [],
|
|
531
793
|
});
|
|
532
|
-
const operatorPrompt =
|
|
794
|
+
const operatorPrompt = OPERATOR_INPUT_PROMPT;
|
|
533
795
|
const hintLineCount = resolveOperatorInputDisplayMode({
|
|
534
796
|
openQuestions: viewModel.getSnapshot().operatorContext?.openQuestions ?? [],
|
|
535
797
|
}).hintLines.length;
|
|
@@ -565,6 +827,8 @@ describe('IssueSessionTui', () => {
|
|
|
565
827
|
});
|
|
566
828
|
const { lastFrame: thinkingFrame, unmount: unmountThinking } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
567
829
|
expect(thinkingFrame() ?? '').toContain('conductor: thinking');
|
|
830
|
+
expect(thinkingFrame() ?? '').toContain('implementer: idle');
|
|
831
|
+
expect(thinkingFrame() ?? '').not.toContain('implementer (implementer)');
|
|
568
832
|
unmountThinking();
|
|
569
833
|
viewModel.setDisplayState({
|
|
570
834
|
workers: {
|
|
@@ -579,6 +843,151 @@ describe('IssueSessionTui', () => {
|
|
|
579
843
|
expect(idleFrame() ?? '').toContain('conductor: idle');
|
|
580
844
|
expect(idleFrame() ?? '').not.toContain('conductor: thinking');
|
|
581
845
|
});
|
|
846
|
+
it('formats worker names and running activity in the workers pane', () => {
|
|
847
|
+
const viewModel = createTuiViewModel();
|
|
848
|
+
viewModel.setDisplayState({
|
|
849
|
+
workers: {
|
|
850
|
+
conductor: { kind: 'conductor', status: 'running' },
|
|
851
|
+
implementer: {
|
|
852
|
+
kind: 'implementer',
|
|
853
|
+
status: 'running',
|
|
854
|
+
activity: 'calling: Shell',
|
|
855
|
+
},
|
|
856
|
+
impl: {
|
|
857
|
+
kind: 'implementer',
|
|
858
|
+
status: 'running',
|
|
859
|
+
activity: 'editing files',
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
conductorOutput: null,
|
|
863
|
+
openQuestions: [],
|
|
864
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
865
|
+
});
|
|
866
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
867
|
+
const frame = lastFrame() ?? '';
|
|
868
|
+
expect(frame).toContain('conductor: thinking');
|
|
869
|
+
expect(frame).toContain('implementer: running (calling: Shell)');
|
|
870
|
+
expect(frame).not.toContain('implementer (implementer)');
|
|
871
|
+
expect(frame).toContain('impl (implementer): running (editing files)');
|
|
872
|
+
});
|
|
873
|
+
it('clears running worker activity on idle transitions within a narrow Workers pane', async () => {
|
|
874
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
875
|
+
configurable: true,
|
|
876
|
+
value: 40,
|
|
877
|
+
});
|
|
878
|
+
const viewModel = createTuiViewModel();
|
|
879
|
+
viewModel.setDisplayState({
|
|
880
|
+
workers: {
|
|
881
|
+
librarian: {
|
|
882
|
+
kind: 'librarian',
|
|
883
|
+
status: 'running',
|
|
884
|
+
activity: 'calling: Shell',
|
|
885
|
+
},
|
|
886
|
+
impl: {
|
|
887
|
+
kind: 'implementer',
|
|
888
|
+
status: 'running',
|
|
889
|
+
activity: '編集中の非常に長い activity の表示幅を確認する',
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
conductorOutput: null,
|
|
893
|
+
openQuestions: [],
|
|
894
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
895
|
+
});
|
|
896
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
897
|
+
const runningFrame = lastFrame() ?? '';
|
|
898
|
+
const runningRows = extractTuiPaneFrameStats(runningFrame, WORKER_PANE_TITLE).contentRows;
|
|
899
|
+
expect(runningRows).toHaveLength(2);
|
|
900
|
+
expect(runningRows.find((line) => line.includes('librarian'))).toContain('librarian: running (calling: Shell)');
|
|
901
|
+
const runningImplRow = runningRows.find((line) => line.includes('impl (implementer)'));
|
|
902
|
+
expect(runningImplRow).toContain('impl (implementer): running');
|
|
903
|
+
expect(runningImplRow).toContain('…');
|
|
904
|
+
expect(runningRows.every((line) => stringWidth(line) === 40)).toBe(true);
|
|
905
|
+
viewModel.setDisplayState({
|
|
906
|
+
workers: {
|
|
907
|
+
librarian: { kind: 'librarian', status: 'idle' },
|
|
908
|
+
impl: { kind: 'implementer', status: 'idle' },
|
|
909
|
+
},
|
|
910
|
+
conductorOutput: null,
|
|
911
|
+
openQuestions: [],
|
|
912
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
913
|
+
});
|
|
914
|
+
await flushInkStdin();
|
|
915
|
+
const idleFrame = lastFrame() ?? '';
|
|
916
|
+
const idleRows = extractTuiPaneFrameStats(idleFrame, WORKER_PANE_TITLE).contentRows;
|
|
917
|
+
expect(idleRows).toHaveLength(2);
|
|
918
|
+
expect(idleRows.find((line) => line.includes('librarian'))).toContain('librarian: idle');
|
|
919
|
+
expect(idleRows.find((line) => line.includes('impl (implementer)'))).toContain('impl (implementer): idle');
|
|
920
|
+
expect(idleFrame).not.toContain('calling: Shell');
|
|
921
|
+
expect(idleFrame).not.toContain('編集中の');
|
|
922
|
+
expect(idleRows.every((line) => stringWidth(line) === 40)).toBe(true);
|
|
923
|
+
});
|
|
924
|
+
it('keeps status visible when a long worker name and kind are truncated at 40 columns', async () => {
|
|
925
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
926
|
+
configurable: true,
|
|
927
|
+
value: 40,
|
|
928
|
+
});
|
|
929
|
+
const name = 'worker-name-longer-than-workers-content-width';
|
|
930
|
+
const kind = 'kind-name-also-much-longer-than-the-content-width';
|
|
931
|
+
const viewModel = createTuiViewModel();
|
|
932
|
+
viewModel.setDisplayState({
|
|
933
|
+
workers: {
|
|
934
|
+
[name]: { kind, status: 'running', activity: 'calling: a very long tool name' },
|
|
935
|
+
},
|
|
936
|
+
conductorOutput: null,
|
|
937
|
+
openQuestions: [],
|
|
938
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
939
|
+
});
|
|
940
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
941
|
+
const runningRow = extractTuiPaneFrameStats(lastFrame() ?? '', WORKER_PANE_TITLE).contentRows[0] ?? '';
|
|
942
|
+
expect(runningRow).toContain(': running');
|
|
943
|
+
expect(runningRow).toContain('…');
|
|
944
|
+
expect(runningRow).not.toContain(name);
|
|
945
|
+
expect(stringWidth(runningRow)).toBe(40);
|
|
946
|
+
viewModel.setDisplayState({
|
|
947
|
+
workers: {
|
|
948
|
+
[name]: { kind, status: 'idle' },
|
|
949
|
+
},
|
|
950
|
+
conductorOutput: null,
|
|
951
|
+
openQuestions: [],
|
|
952
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
953
|
+
});
|
|
954
|
+
await flushInkStdin();
|
|
955
|
+
const idleRow = extractTuiPaneFrameStats(lastFrame() ?? '', WORKER_PANE_TITLE).contentRows[0] ?? '';
|
|
956
|
+
expect(idleRow).toContain(': idle');
|
|
957
|
+
expect(idleRow).toContain('…');
|
|
958
|
+
expect(stringWidth(idleRow)).toBe(40);
|
|
959
|
+
});
|
|
960
|
+
it('strips open SGR and OSC 8 sequences from truncated activity before rendering', () => {
|
|
961
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
962
|
+
configurable: true,
|
|
963
|
+
value: 40,
|
|
964
|
+
});
|
|
965
|
+
const activity = [
|
|
966
|
+
'\u001B[31m',
|
|
967
|
+
'colored activity '.repeat(8),
|
|
968
|
+
'\u001B[0m',
|
|
969
|
+
'\u001B]8;;https://example.com/worker-activity\u0007',
|
|
970
|
+
'linked activity '.repeat(8),
|
|
971
|
+
'\u001B]8;;\u0007',
|
|
972
|
+
].join('');
|
|
973
|
+
const viewModel = createTuiViewModel();
|
|
974
|
+
viewModel.setDisplayState({
|
|
975
|
+
workers: {
|
|
976
|
+
impl: { kind: 'implementer', status: 'running', activity },
|
|
977
|
+
},
|
|
978
|
+
conductorOutput: null,
|
|
979
|
+
openQuestions: [],
|
|
980
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
981
|
+
});
|
|
982
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
983
|
+
const frame = lastFrame() ?? '';
|
|
984
|
+
const workerRow = extractTuiPaneFrameStats(frame, WORKER_PANE_TITLE).contentRows[0] ?? '';
|
|
985
|
+
expect(workerRow).toContain(': running');
|
|
986
|
+
expect(workerRow).toContain('…');
|
|
987
|
+
expect(stringWidth(workerRow)).toBe(40);
|
|
988
|
+
expect(frame).not.toContain('\u001B');
|
|
989
|
+
expect(frame).not.toContain('https://example.com/worker-activity');
|
|
990
|
+
});
|
|
582
991
|
it('does not bleed activity log text onto pane border lines', () => {
|
|
583
992
|
const terminalRows = 24;
|
|
584
993
|
const liveFrameRows = terminalRows - 1;
|
|
@@ -640,7 +1049,7 @@ describe('IssueSessionTui', () => {
|
|
|
640
1049
|
const frame = lastFrame() ?? '';
|
|
641
1050
|
expect(frame.split('\n')).toHaveLength(11);
|
|
642
1051
|
expect(Math.max(...frame.split('\n').map((line) => line.trimEnd().length))).toBeLessThanOrEqual(40);
|
|
643
|
-
expect(frame).toContain(
|
|
1052
|
+
expect(frame).toContain(OPERATOR_INPUT_PROMPT);
|
|
644
1053
|
terminalSizeStore.dispose();
|
|
645
1054
|
});
|
|
646
1055
|
it('coalesces staged same-row shrink before redrawing the pane frame', async () => {
|
|
@@ -685,7 +1094,7 @@ describe('IssueSessionTui', () => {
|
|
|
685
1094
|
expect(finalFrame).not.toBe(initialFrame);
|
|
686
1095
|
expect(Math.max(...finalFrame.split('\n').map((line) => line.trimEnd().length))).toBeLessThanOrEqual(60);
|
|
687
1096
|
expect(finalFrame).toContain(WORKER_PANE_TITLE);
|
|
688
|
-
expect(finalFrame).toContain(
|
|
1097
|
+
expect(finalFrame).toContain(OPERATOR_INPUT_PROMPT);
|
|
689
1098
|
}
|
|
690
1099
|
finally {
|
|
691
1100
|
unsubscribe();
|
|
@@ -720,15 +1129,17 @@ describe('IssueSessionTui', () => {
|
|
|
720
1129
|
hintLineCount,
|
|
721
1130
|
inputDisplayLineCount: 1,
|
|
722
1131
|
});
|
|
1132
|
+
const desiredWorkerPaneHeight = computeWorkerPaneHeight(0);
|
|
723
1133
|
const paneHeights = resolvePaneHeights({
|
|
724
1134
|
liveFrameRows: 11,
|
|
725
|
-
|
|
1135
|
+
workerPaneHeight: desiredWorkerPaneHeight,
|
|
1136
|
+
activityPaneHeight: 11 - desiredWorkerPaneHeight - requestedInputPaneHeight,
|
|
726
1137
|
openQuestionsPaneHeight: 0,
|
|
727
1138
|
inputPaneHeight: requestedInputPaneHeight,
|
|
728
1139
|
});
|
|
729
1140
|
expect(paneHeights).toEqual({
|
|
730
|
-
workerPaneHeight:
|
|
731
|
-
activityPaneHeight:
|
|
1141
|
+
workerPaneHeight: 3,
|
|
1142
|
+
activityPaneHeight: 5,
|
|
732
1143
|
openQuestionsPaneHeight: 0,
|
|
733
1144
|
inputPaneHeight: 3,
|
|
734
1145
|
});
|
|
@@ -745,7 +1156,8 @@ describe('IssueSessionTui', () => {
|
|
|
745
1156
|
expect(inputLine.lineIndex).toBeLessThan(lines.length);
|
|
746
1157
|
expect(inputLine.inputStartX).toBeGreaterThanOrEqual(0);
|
|
747
1158
|
expect(frame).toContain('Orchestration');
|
|
748
|
-
expect(frame).toContain(
|
|
1159
|
+
expect(frame).toContain(OPERATOR_INPUT_PROMPT);
|
|
1160
|
+
expect(frame).not.toContain('operator> ');
|
|
749
1161
|
}
|
|
750
1162
|
finally {
|
|
751
1163
|
terminalSizeStore.dispose();
|
|
@@ -759,15 +1171,40 @@ describe('IssueSessionTui', () => {
|
|
|
759
1171
|
expect(frame).toContain('(活動ログなし)');
|
|
760
1172
|
expect(frame).not.toContain('Open questions');
|
|
761
1173
|
expect(frame).not.toContain('(未回答なし)');
|
|
762
|
-
expect(frame).toContain(
|
|
1174
|
+
expect(frame).toContain(OPERATOR_INPUT_PROMPT);
|
|
763
1175
|
expect(frame).not.toContain('任意入力');
|
|
764
1176
|
expect(frame).not.toContain('/reconnect');
|
|
765
1177
|
});
|
|
1178
|
+
it.each([1, 2, 5])('sizes the Workers pane without blank rows for %i workers plus conductor', (workerCount) => {
|
|
1179
|
+
const viewModel = createTuiViewModel();
|
|
1180
|
+
const workers = {
|
|
1181
|
+
conductor: { kind: 'conductor', status: 'idle' },
|
|
1182
|
+
};
|
|
1183
|
+
for (let index = 1; index <= workerCount; index++) {
|
|
1184
|
+
workers[`worker-${index}`] = {
|
|
1185
|
+
kind: `worker-${index}`,
|
|
1186
|
+
status: 'idle',
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
viewModel.setDisplayState({
|
|
1190
|
+
workers,
|
|
1191
|
+
conductorOutput: null,
|
|
1192
|
+
openQuestions: [],
|
|
1193
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
1194
|
+
});
|
|
1195
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
1196
|
+
const frame = lastFrame() ?? '';
|
|
1197
|
+
const stats = extractTuiPaneFrameStats(frame, WORKER_PANE_TITLE);
|
|
1198
|
+
expect(stats.contentRows).toHaveLength(workerCount + 1);
|
|
1199
|
+
expect(stats.blankRows).toHaveLength(0);
|
|
1200
|
+
expect(frame).toContain('conductor: idle');
|
|
1201
|
+
expect(frame).toContain(`worker-${workerCount}: idle`);
|
|
1202
|
+
});
|
|
766
1203
|
it('switches the lower UI between no-question and question modes', async () => {
|
|
767
1204
|
const viewModel = createTuiViewModel();
|
|
768
1205
|
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
769
1206
|
expect(lastFrame() ?? '').not.toContain('Open questions');
|
|
770
|
-
expect(lastFrame() ?? '').toContain(
|
|
1207
|
+
expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
|
|
771
1208
|
expect(lastFrame() ?? '').not.toContain('任意入力');
|
|
772
1209
|
viewModel.setDisplayState({
|
|
773
1210
|
workers: {},
|
|
@@ -787,7 +1224,7 @@ describe('IssueSessionTui', () => {
|
|
|
787
1224
|
});
|
|
788
1225
|
await flushInkStdin();
|
|
789
1226
|
expect(lastFrame() ?? '').not.toContain('Open questions');
|
|
790
|
-
expect(lastFrame() ?? '').toContain(
|
|
1227
|
+
expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
|
|
791
1228
|
expect(lastFrame() ?? '').not.toContain('任意入力');
|
|
792
1229
|
});
|
|
793
1230
|
it.each([
|
|
@@ -836,7 +1273,7 @@ describe('IssueSessionTui', () => {
|
|
|
836
1273
|
});
|
|
837
1274
|
if (restoredQuestions.length === 1) {
|
|
838
1275
|
expect(lastFrame() ?? '').not.toContain('Open questions');
|
|
839
|
-
expect(lastFrame() ?? '').toContain(
|
|
1276
|
+
expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
|
|
840
1277
|
expect(lastFrame() ?? '').not.toContain('任意入力');
|
|
841
1278
|
}
|
|
842
1279
|
else {
|
|
@@ -846,20 +1283,32 @@ describe('IssueSessionTui', () => {
|
|
|
846
1283
|
setOperatorQuestions([]);
|
|
847
1284
|
await flushInkStdin();
|
|
848
1285
|
expect(lastFrame() ?? '').not.toContain('Open questions');
|
|
849
|
-
expect(lastFrame() ?? '').toContain(
|
|
1286
|
+
expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
|
|
850
1287
|
expect(lastFrame() ?? '').not.toContain('任意入力');
|
|
851
1288
|
}
|
|
852
1289
|
});
|
|
853
|
-
it('
|
|
1290
|
+
it('separates changing activity labels and keeps consecutive harness entries together', () => {
|
|
854
1291
|
const viewModel = createTuiViewModel();
|
|
855
|
-
viewModel.appendActivityLog('
|
|
856
|
-
viewModel.appendActivityLog('harness', 'tel');
|
|
1292
|
+
viewModel.appendActivityLog('harness', 'tel-1');
|
|
1293
|
+
viewModel.appendActivityLog('harness', 'tel-2');
|
|
857
1294
|
viewModel.appendActivityLog('observation', 'obs');
|
|
1295
|
+
viewModel.appendActivityLog('operator', 'op');
|
|
858
1296
|
viewModel.appendActivityLog('conductor', 'cond');
|
|
859
1297
|
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
860
1298
|
const frame = lastFrame() ?? '';
|
|
861
|
-
|
|
862
|
-
|
|
1299
|
+
const lines = frame.split('\n');
|
|
1300
|
+
const harnessFirst = lines.findIndex((line) => line.includes('[harness] tel-1'));
|
|
1301
|
+
const harnessSecond = lines.findIndex((line) => line.includes('[harness] tel-2'));
|
|
1302
|
+
const observation = lines.findIndex((line) => line.includes('[observation] obs'));
|
|
1303
|
+
const operator = lines.findIndex((line) => line.includes('[operator] op'));
|
|
1304
|
+
const conductor = lines.findIndex((line) => line.includes('[conductor] cond'));
|
|
1305
|
+
expect(harnessFirst).toBeGreaterThanOrEqual(0);
|
|
1306
|
+
expect(harnessSecond).toBe(harnessFirst + 1);
|
|
1307
|
+
expect(observation).toBe(harnessSecond + 2);
|
|
1308
|
+
expect(operator).toBe(observation + 2);
|
|
1309
|
+
expect(conductor).toBe(operator + 2);
|
|
1310
|
+
expect(frame).toContain('[harness] tel-1');
|
|
1311
|
+
expect(frame).toContain('[harness] tel-2');
|
|
863
1312
|
expect(frame).toContain('[observation] obs');
|
|
864
1313
|
expect(frame).toContain('[conductor] cond');
|
|
865
1314
|
});
|
|
@@ -1054,6 +1503,73 @@ describe('IssueSessionTui', () => {
|
|
|
1054
1503
|
expect(secondIndex).toBeGreaterThan(firstIndex + 1);
|
|
1055
1504
|
});
|
|
1056
1505
|
describe('open questions selection (stdin integration)', () => {
|
|
1506
|
+
it('scrolls the selected full context from top to bottom without taking suggestion arrows', async () => {
|
|
1507
|
+
const viewModel = createTuiViewModel();
|
|
1508
|
+
viewModel.setDisplayState({
|
|
1509
|
+
workers: {},
|
|
1510
|
+
conductorOutput: null,
|
|
1511
|
+
openQuestions: [
|
|
1512
|
+
createOpenQuestion({
|
|
1513
|
+
id: 'inq-long',
|
|
1514
|
+
question: 'Choose a mode',
|
|
1515
|
+
responseType: 'choice',
|
|
1516
|
+
context: LONG_QUESTION_CONTEXT,
|
|
1517
|
+
choices: [
|
|
1518
|
+
{ id: 'safe', label: 'Safe mode' },
|
|
1519
|
+
{ id: 'secure', label: 'Secure mode' },
|
|
1520
|
+
],
|
|
1521
|
+
}),
|
|
1522
|
+
createOpenQuestion({
|
|
1523
|
+
id: 'inq-next',
|
|
1524
|
+
question: 'Anything else?',
|
|
1525
|
+
}),
|
|
1526
|
+
],
|
|
1527
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
1528
|
+
});
|
|
1529
|
+
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
1530
|
+
const initialFrame = lastFrame() ?? '';
|
|
1531
|
+
expect(initialFrame).toContain('context-line-00');
|
|
1532
|
+
expect(initialFrame).not.toContain('context-line-39');
|
|
1533
|
+
expect(initialFrame).toContain('Shift+PgUp/PgDnで本文スクロール');
|
|
1534
|
+
expect(initialFrame).toContain('で選択) · Shift+PgUp/PgDn');
|
|
1535
|
+
expect(initialFrame).toContain('› safe: Safe mode');
|
|
1536
|
+
stdin.write(INK_TEST_KEYS.shiftPageDown.repeat(2));
|
|
1537
|
+
await flushInkStdin();
|
|
1538
|
+
const scrolledFrame = lastFrame() ?? '';
|
|
1539
|
+
expect(scrolledFrame).not.toContain('context-line-00');
|
|
1540
|
+
expect(scrolledFrame).toContain('context-line-01');
|
|
1541
|
+
expect(scrolledFrame).toContain('› safe: Safe mode');
|
|
1542
|
+
stdin.write(INK_TEST_KEYS.downArrow);
|
|
1543
|
+
await flushInkStdin();
|
|
1544
|
+
expect(lastFrame() ?? '').toContain('› secure: Secure mode');
|
|
1545
|
+
expect(lastFrame() ?? '').toContain('context-line-01');
|
|
1546
|
+
stdin.write('/');
|
|
1547
|
+
await flushInkStdin();
|
|
1548
|
+
expect(lastFrame() ?? '').toContain('› /reconnect');
|
|
1549
|
+
stdin.write(INK_TEST_KEYS.shiftPageDown.repeat(200));
|
|
1550
|
+
await flushInkStdin();
|
|
1551
|
+
const bottomFrame = lastFrame() ?? '';
|
|
1552
|
+
expect(bottomFrame).toContain('context-line-39');
|
|
1553
|
+
expect(bottomFrame).not.toContain('context-line-00');
|
|
1554
|
+
expect(bottomFrame).toContain('› /reconnect');
|
|
1555
|
+
stdin.write(INK_TEST_KEYS.shiftDownArrow);
|
|
1556
|
+
await flushInkStdin();
|
|
1557
|
+
expect(lastFrame() ?? '').toContain('2/2');
|
|
1558
|
+
stdin.write(INK_TEST_KEYS.shiftUpArrow);
|
|
1559
|
+
await flushInkStdin();
|
|
1560
|
+
const resetFrame = lastFrame() ?? '';
|
|
1561
|
+
expect(resetFrame).toContain('1/2');
|
|
1562
|
+
expect(resetFrame).toContain('context-line-00');
|
|
1563
|
+
expect(resetFrame).not.toContain('context-line-39');
|
|
1564
|
+
stdin.write(INK_TEST_KEYS.shiftPageDown.repeat(200));
|
|
1565
|
+
await flushInkStdin();
|
|
1566
|
+
expect(lastFrame() ?? '').toContain('context-line-39');
|
|
1567
|
+
stdin.write(INK_TEST_KEYS.shiftPageUp.repeat(200));
|
|
1568
|
+
await flushInkStdin();
|
|
1569
|
+
const topFrame = lastFrame() ?? '';
|
|
1570
|
+
expect(topFrame).toContain('context-line-00');
|
|
1571
|
+
expect(topFrame).not.toContain('context-line-39');
|
|
1572
|
+
});
|
|
1057
1573
|
it('cycles selection with Shift+arrow keys', async () => {
|
|
1058
1574
|
const viewModel = createTuiViewModel();
|
|
1059
1575
|
viewModel.setDisplayState({
|
|
@@ -1197,13 +1713,13 @@ describe('IssueSessionTui', () => {
|
|
|
1197
1713
|
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
1198
1714
|
const pinnedFrame = lastFrame() ?? '';
|
|
1199
1715
|
expect(pinnedFrame).toContain('[harness] line-29');
|
|
1200
|
-
expect(pinnedFrame).not.toContain('[harness] line-
|
|
1716
|
+
expect(pinnedFrame).not.toContain('[harness] line-14');
|
|
1201
1717
|
expect(pinnedFrame).not.toContain(SCROLL_HINT);
|
|
1202
1718
|
stdin.write(INK_TEST_KEYS.pageUp);
|
|
1203
1719
|
await flushInkStdin();
|
|
1204
1720
|
const scrolledFrame = lastFrame() ?? '';
|
|
1205
1721
|
expect(scrolledFrame).toContain(SCROLL_HINT);
|
|
1206
|
-
expect(scrolledFrame).toContain('[harness] line-
|
|
1722
|
+
expect(scrolledFrame).toContain('[harness] line-14');
|
|
1207
1723
|
expect(scrolledFrame).not.toContain('[harness] line-29');
|
|
1208
1724
|
});
|
|
1209
1725
|
it('returns to latest lines on End and clears scroll hint', async () => {
|
|
@@ -1225,11 +1741,11 @@ describe('IssueSessionTui', () => {
|
|
|
1225
1741
|
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
1226
1742
|
stdin.write(INK_TEST_KEYS.pageUp);
|
|
1227
1743
|
await flushInkStdin();
|
|
1228
|
-
expect(lastFrame() ?? '').toContain('[harness] line-
|
|
1744
|
+
expect(lastFrame() ?? '').toContain('[harness] line-14');
|
|
1229
1745
|
viewModel.appendActivityLog('harness', 'newest-marker');
|
|
1230
1746
|
await flushInkStdin();
|
|
1231
1747
|
const detachedFrame = lastFrame() ?? '';
|
|
1232
|
-
expect(detachedFrame).toContain('[harness] line-
|
|
1748
|
+
expect(detachedFrame).toContain('[harness] line-14');
|
|
1233
1749
|
expect(detachedFrame).not.toContain('[harness] newest-marker');
|
|
1234
1750
|
stdin.write(INK_TEST_KEYS.end);
|
|
1235
1751
|
await flushInkStdin();
|
|
@@ -1258,7 +1774,7 @@ describe('IssueSessionTui', () => {
|
|
|
1258
1774
|
const ctrlPageUpFrame = lastFrame() ?? '';
|
|
1259
1775
|
expect(ctrlPageUpFrame).toContain('typed');
|
|
1260
1776
|
expect(ctrlPageUpFrame).toContain(SCROLL_HINT);
|
|
1261
|
-
expect(ctrlPageUpFrame).toContain('[harness] line-
|
|
1777
|
+
expect(ctrlPageUpFrame).toContain('[harness] line-14');
|
|
1262
1778
|
expect(ctrlPageUpFrame).not.toContain('[harness] line-29');
|
|
1263
1779
|
});
|
|
1264
1780
|
});
|