@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.
Files changed (86) hide show
  1. package/README.md +9 -3
  2. package/config.example.yaml +3 -3
  3. package/dist/format-session-summary.d.ts.map +1 -1
  4. package/dist/format-session-summary.js +51 -45
  5. package/dist/format-session-summary.js.map +1 -1
  6. package/dist/index.js +5 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/session-log-lines.js +1 -1
  9. package/dist/session-log-lines.js.map +1 -1
  10. package/dist/tui/activity-log.d.ts +1 -1
  11. package/dist/tui/activity-log.d.ts.map +1 -1
  12. package/dist/tui/activity-log.js +16 -2
  13. package/dist/tui/activity-log.js.map +1 -1
  14. package/dist/tui/create-issue-session-tui-host.d.ts.map +1 -1
  15. package/dist/tui/create-issue-session-tui-host.js +1 -1
  16. package/dist/tui/create-issue-session-tui-host.js.map +1 -1
  17. package/dist/tui/format-operator-context.d.ts.map +1 -1
  18. package/dist/tui/format-operator-context.js +3 -3
  19. package/dist/tui/format-operator-context.js.map +1 -1
  20. package/dist/tui/ink-test-keys.d.ts +4 -0
  21. package/dist/tui/ink-test-keys.d.ts.map +1 -1
  22. package/dist/tui/ink-test-keys.js +4 -0
  23. package/dist/tui/ink-test-keys.js.map +1 -1
  24. package/dist/tui/issue-session-tui-stream-cursor.test.js +3 -3
  25. package/dist/tui/issue-session-tui-stream-cursor.test.js.map +1 -1
  26. package/dist/tui/issue-session-tui-stream.d.ts.map +1 -1
  27. package/dist/tui/issue-session-tui-stream.js +133 -15
  28. package/dist/tui/issue-session-tui-stream.js.map +1 -1
  29. package/dist/tui/issue-session-tui-stream.test.js +369 -13
  30. package/dist/tui/issue-session-tui-stream.test.js.map +1 -1
  31. package/dist/tui/issue-session-tui.d.ts +3 -2
  32. package/dist/tui/issue-session-tui.d.ts.map +1 -1
  33. package/dist/tui/issue-session-tui.js +205 -21
  34. package/dist/tui/issue-session-tui.js.map +1 -1
  35. package/dist/tui/issue-session-tui.test.js +553 -37
  36. package/dist/tui/issue-session-tui.test.js.map +1 -1
  37. package/dist/tui/open-question-answer-suggestions.d.ts +19 -0
  38. package/dist/tui/open-question-answer-suggestions.d.ts.map +1 -0
  39. package/dist/tui/open-question-answer-suggestions.js +62 -0
  40. package/dist/tui/open-question-answer-suggestions.js.map +1 -0
  41. package/dist/tui/open-questions-pane.d.ts +19 -0
  42. package/dist/tui/open-questions-pane.d.ts.map +1 -1
  43. package/dist/tui/open-questions-pane.js +35 -34
  44. package/dist/tui/open-questions-pane.js.map +1 -1
  45. package/dist/tui/operator-command-suggestions.d.ts +15 -0
  46. package/dist/tui/operator-command-suggestions.d.ts.map +1 -1
  47. package/dist/tui/operator-input-history.d.ts +13 -0
  48. package/dist/tui/operator-input-history.d.ts.map +1 -0
  49. package/dist/tui/operator-input-history.js +51 -0
  50. package/dist/tui/operator-input-history.js.map +1 -0
  51. package/dist/tui/operator-input-layout.d.ts +2 -0
  52. package/dist/tui/operator-input-layout.d.ts.map +1 -1
  53. package/dist/tui/operator-input-layout.js +73 -0
  54. package/dist/tui/operator-input-layout.js.map +1 -1
  55. package/dist/tui/operator-text-area.d.ts +6 -4
  56. package/dist/tui/operator-text-area.d.ts.map +1 -1
  57. package/dist/tui/operator-text-area.js +17 -5
  58. package/dist/tui/operator-text-area.js.map +1 -1
  59. package/dist/tui/operator-text-area.test-double.d.ts +1 -1
  60. package/dist/tui/operator-text-area.test-double.d.ts.map +1 -1
  61. package/dist/tui/operator-text-area.test-double.js +15 -14
  62. package/dist/tui/operator-text-area.test-double.js.map +1 -1
  63. package/dist/tui/operator-text-area.test.js +17 -3
  64. package/dist/tui/operator-text-area.test.js.map +1 -1
  65. package/dist/tui/pane-layout.d.ts +2 -0
  66. package/dist/tui/pane-layout.d.ts.map +1 -1
  67. package/dist/tui/pane-layout.js +7 -3
  68. package/dist/tui/pane-layout.js.map +1 -1
  69. package/dist/tui/stream-layout.d.ts +3 -3
  70. package/dist/tui/stream-layout.d.ts.map +1 -1
  71. package/dist/tui/stream-layout.js +6 -6
  72. package/dist/tui/stream-layout.js.map +1 -1
  73. package/dist/tui/tui-layout-constants.d.ts +4 -2
  74. package/dist/tui/tui-layout-constants.d.ts.map +1 -1
  75. package/dist/tui/tui-layout-constants.js +4 -2
  76. package/dist/tui/tui-layout-constants.js.map +1 -1
  77. package/dist/tui/tui-view-model.d.ts.map +1 -1
  78. package/dist/tui/tui-view-model.js +2 -8
  79. package/dist/tui/tui-view-model.js.map +1 -1
  80. package/dist/tui/worker-display-name.d.ts +2 -0
  81. package/dist/tui/worker-display-name.d.ts.map +1 -0
  82. package/dist/tui/worker-display-name.js +4 -0
  83. package/dist/tui/worker-display-name.js.map +1 -0
  84. package/docs/config.md +92 -16
  85. package/docs/settings.md +3 -3
  86. 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, WORKER_PANE_HEIGHT, WORKER_PANE_TITLE, } from './tui-layout-constants.js';
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('operator>') ? index : -1))
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('operator> ');
29
- const inputStartX = promptIndex >= 0 ? promptIndex + 'operator> '.length : -1;
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('› /close');
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('› /reconnect');
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('operator> /close');
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('operator> /exit');
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('operator> /unknown');
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('operator>');
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('operator>');
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('operator>');
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('operator>');
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('operator>');
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 = 'operator> ';
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 = 'operator> ';
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('operator>');
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('operator>');
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
- activityPaneHeight: 11 - WORKER_PANE_HEIGHT - requestedInputPaneHeight,
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: 5,
731
- activityPaneHeight: 3,
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('operator>');
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('operator>');
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('operator>');
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('operator>');
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('operator>');
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('operator>');
1286
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
850
1287
  expect(lastFrame() ?? '').not.toContain('任意入力');
851
1288
  }
852
1289
  });
853
- it('renders all activity log label kinds with distinct markers', () => {
1290
+ it('separates changing activity labels and keeps consecutive harness entries together', () => {
854
1291
  const viewModel = createTuiViewModel();
855
- viewModel.appendActivityLog('operator', 'op');
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
- expect(frame).toContain('[operator] op');
862
- expect(frame).toContain('[harness] tel');
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-16');
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-16');
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-16');
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-16');
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-16');
1777
+ expect(ctrlPageUpFrame).toContain('[harness] line-14');
1262
1778
  expect(ctrlPageUpFrame).not.toContain('[harness] line-29');
1263
1779
  });
1264
1780
  });