@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
@@ -8,7 +8,9 @@ import { render as renderInk } from 'ink';
8
8
  import { cleanup, render } from 'ink-testing-library';
9
9
  import { IssueSessionTuiStream } from './issue-session-tui-stream.js';
10
10
  import { createTuiViewModel } from './tui-view-model.js';
11
+ import { extractTuiPaneFrameStats } from './orchestration-pane-frame.js';
11
12
  import { flushInkStdin, INK_TEST_KEYS } from './ink-test-keys.js';
13
+ import { OPERATOR_INPUT_PROMPT } from './tui-layout-constants.js';
12
14
  import { createTuiResizeController, createTuiTerminalSizeStore, TUI_RESIZE_SETTLE_MS, TUI_RESIZE_SHRINK_COALESCE_MS, } from './tui-terminal-size.js';
13
15
  import { createTuiResizeRecovery, TUI_FULL_REDRAW_SEQUENCE, } from './tui-resize-recovery.js';
14
16
  class ResizeSource extends EventEmitter {
@@ -48,6 +50,7 @@ function createOpenQuestion(overrides) {
48
50
  }
49
51
  const ISSUE_URL = 'https://github.com/otolab/agents-ensemble/issues/261';
50
52
  const NON_CANONICAL_ISSUE_URL = 'http://github.com/otolab/agents-ensemble/issues/261/';
53
+ const LONG_QUESTION_CONTEXT = Array.from({ length: 40 }, (_, index) => `context-line-${String(index).padStart(2, '0')} ${'wrapped segment '.repeat(6)}`).join('\n');
51
54
  describe('IssueSessionTuiStream', () => {
52
55
  beforeEach(() => {
53
56
  Object.defineProperty(process.stdout, 'rows', {
@@ -77,6 +80,153 @@ describe('IssueSessionTuiStream', () => {
77
80
  await flushInkStdin();
78
81
  expect(onSubmit).toHaveBeenCalledWith('/ex', undefined);
79
82
  });
83
+ it('navigates submitted history at visual boundaries and restores an edited draft', async () => {
84
+ const viewModel = createTuiViewModel();
85
+ const onSubmit = vi.fn();
86
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, onSubmit: onSubmit }));
87
+ stdin.write('A');
88
+ await flushInkStdin();
89
+ stdin.write('\r');
90
+ await flushInkStdin();
91
+ expect(onSubmit).toHaveBeenCalledWith('A', undefined);
92
+ stdin.write('\u001B[200~line1\nline2\u001B[201~');
93
+ await flushInkStdin();
94
+ stdin.write(INK_TEST_KEYS.upArrow);
95
+ await flushInkStdin();
96
+ stdin.write('X');
97
+ await flushInkStdin();
98
+ stdin.write(INK_TEST_KEYS.home);
99
+ await flushInkStdin();
100
+ stdin.write(INK_TEST_KEYS.upArrow);
101
+ await flushInkStdin();
102
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}A`);
103
+ stdin.write("'");
104
+ await flushInkStdin();
105
+ stdin.write('\r');
106
+ await flushInkStdin();
107
+ expect(onSubmit).toHaveBeenLastCalledWith("A'", undefined);
108
+ stdin.write(INK_TEST_KEYS.downArrow);
109
+ await flushInkStdin();
110
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}line1X`);
111
+ expect(lastFrame() ?? '').toContain('line2');
112
+ stdin.write(INK_TEST_KEYS.upArrow);
113
+ await flushInkStdin();
114
+ stdin.write(INK_TEST_KEYS.home);
115
+ await flushInkStdin();
116
+ stdin.write(INK_TEST_KEYS.upArrow);
117
+ await flushInkStdin();
118
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}A'`);
119
+ stdin.write(INK_TEST_KEYS.upArrow);
120
+ await flushInkStdin();
121
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}A`);
122
+ });
123
+ it('keeps an Open Question answer completion after a historical submit', async () => {
124
+ const viewModel = createTuiViewModel();
125
+ const question = createOpenQuestion({
126
+ id: 'inq-yes-no',
127
+ question: 'Proceed?',
128
+ responseType: 'yes_no',
129
+ });
130
+ viewModel.setDisplayState({
131
+ workers: {},
132
+ conductorOutput: null,
133
+ openQuestions: [question],
134
+ dispatchHold: { hold: false, heldEventCount: 0 },
135
+ });
136
+ const onSubmit = vi.fn();
137
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, onSubmit: onSubmit }));
138
+ stdin.write('previous input');
139
+ await flushInkStdin();
140
+ stdin.write('\r');
141
+ await flushInkStdin();
142
+ stdin.write('unsent draft');
143
+ await flushInkStdin();
144
+ stdin.write(INK_TEST_KEYS.upArrow);
145
+ await flushInkStdin();
146
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}previous input`);
147
+ stdin.write(' edited');
148
+ await flushInkStdin();
149
+ stdin.write('\r');
150
+ await flushInkStdin();
151
+ expect(onSubmit).toHaveBeenLastCalledWith('previous input edited', {
152
+ targetOpenQuestionId: question.id,
153
+ });
154
+ stdin.write('\r');
155
+ await flushInkStdin();
156
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}yes`);
157
+ stdin.write(INK_TEST_KEYS.downArrow);
158
+ await flushInkStdin();
159
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}yes`);
160
+ expect(lastFrame() ?? '').not.toContain(`${OPERATOR_INPUT_PROMPT}unsent draft`);
161
+ stdin.write('\r');
162
+ await flushInkStdin();
163
+ expect(onSubmit).toHaveBeenLastCalledWith('yes', {
164
+ targetOpenQuestionId: question.id,
165
+ });
166
+ });
167
+ it('moves within a soft-wrapped input and enters history only at its visual boundaries', async () => {
168
+ Object.defineProperty(process.stdout, 'columns', {
169
+ configurable: true,
170
+ value: 44,
171
+ });
172
+ const viewModel = createTuiViewModel();
173
+ const onSubmit = vi.fn();
174
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, onSubmit: onSubmit }));
175
+ const olderEntry = 'b'.repeat(90);
176
+ stdin.write(olderEntry);
177
+ await flushInkStdin();
178
+ stdin.write('\r');
179
+ await flushInkStdin();
180
+ const wrappedDraft = 'a'.repeat(90);
181
+ stdin.write(wrappedDraft);
182
+ await flushInkStdin();
183
+ stdin.write(INK_TEST_KEYS.upArrow);
184
+ await flushInkStdin();
185
+ stdin.write('X');
186
+ await flushInkStdin();
187
+ stdin.write(INK_TEST_KEYS.downArrow);
188
+ await flushInkStdin();
189
+ stdin.write('Y');
190
+ await flushInkStdin();
191
+ stdin.write(INK_TEST_KEYS.upArrow);
192
+ await flushInkStdin();
193
+ stdin.write(INK_TEST_KEYS.upArrow);
194
+ await flushInkStdin();
195
+ stdin.write(INK_TEST_KEYS.upArrow);
196
+ await flushInkStdin();
197
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}${olderEntry.slice(0, 10)}`);
198
+ stdin.write(INK_TEST_KEYS.upArrow);
199
+ await flushInkStdin();
200
+ stdin.write(INK_TEST_KEYS.downArrow);
201
+ await flushInkStdin();
202
+ stdin.write(INK_TEST_KEYS.downArrow);
203
+ await flushInkStdin();
204
+ stdin.write('\r');
205
+ await flushInkStdin();
206
+ const editedWrappedDraft = onSubmit.mock.calls.at(-1)?.[0];
207
+ expect(editedWrappedDraft).toHaveLength(wrappedDraft.length + 2);
208
+ expect(editedWrappedDraft.indexOf('Y') - editedWrappedDraft.indexOf('X')).toBeGreaterThan(1);
209
+ expect(editedWrappedDraft.replace(/[XY]/g, '')).toBe(wrappedDraft);
210
+ stdin.write('pending draft');
211
+ await flushInkStdin();
212
+ stdin.write(INK_TEST_KEYS.upArrow);
213
+ await flushInkStdin();
214
+ stdin.write(INK_TEST_KEYS.downArrow);
215
+ await flushInkStdin();
216
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}pending draft`);
217
+ stdin.write(INK_TEST_KEYS.upArrow);
218
+ await flushInkStdin();
219
+ stdin.write(INK_TEST_KEYS.upArrow);
220
+ await flushInkStdin();
221
+ stdin.write(INK_TEST_KEYS.upArrow);
222
+ await flushInkStdin();
223
+ stdin.write(INK_TEST_KEYS.upArrow);
224
+ await flushInkStdin();
225
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}${olderEntry.slice(0, 10)}`);
226
+ stdin.write('\r');
227
+ await flushInkStdin();
228
+ expect(onSubmit).toHaveBeenLastCalledWith(olderEntry, undefined);
229
+ });
80
230
  it('selects a slash suggestion and Tab completes it without submitting', async () => {
81
231
  const viewModel = createTuiViewModel();
82
232
  const onSubmit = vi.fn();
@@ -86,17 +236,116 @@ describe('IssueSessionTuiStream', () => {
86
236
  expect(lastFrame() ?? '').toContain('› /close');
87
237
  stdin.write(INK_TEST_KEYS.tab);
88
238
  await flushInkStdin();
89
- expect(lastFrame() ?? '').toContain('operator> /close');
239
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}/close`);
240
+ expect(lastFrame() ?? '').not.toContain('operator> ');
90
241
  expect(onSubmit).not.toHaveBeenCalled();
91
242
  stdin.write('\r');
92
243
  await flushInkStdin();
93
244
  expect(onSubmit).toHaveBeenCalledWith('/close', undefined);
94
245
  });
246
+ it.each([
247
+ {
248
+ name: 'choice',
249
+ question: createOpenQuestion({
250
+ id: 'inq-choice',
251
+ question: 'Choose a mode',
252
+ responseType: 'choice',
253
+ choices: [
254
+ { id: 'safe', label: 'Safe mode' },
255
+ { id: 'secure', label: 'Secure mode' },
256
+ { id: 'fast', label: 'Fast mode' },
257
+ ],
258
+ }),
259
+ prefix: 's',
260
+ completedValue: 'secure',
261
+ expectedSuggestion: 'safe: Safe mode',
262
+ hasMultipleSuggestions: true,
263
+ },
264
+ {
265
+ name: 'yes_no',
266
+ question: createOpenQuestion({
267
+ id: 'inq-yes-no',
268
+ question: 'Proceed?',
269
+ responseType: 'yes_no',
270
+ }),
271
+ prefix: 'n',
272
+ completedValue: 'no',
273
+ expectedSuggestion: 'no — いいえ',
274
+ hasMultipleSuggestions: false,
275
+ },
276
+ ])('completes and submits the selected $name answer in stream mode', async ({ question, prefix, completedValue, expectedSuggestion, hasMultipleSuggestions, }) => {
277
+ const viewModel = createTuiViewModel();
278
+ viewModel.setDisplayState({
279
+ workers: {},
280
+ conductorOutput: null,
281
+ openQuestions: [
282
+ question,
283
+ createOpenQuestion({
284
+ id: 'inq-text',
285
+ question: 'Anything else?',
286
+ }),
287
+ ],
288
+ dispatchHold: { hold: false, heldEventCount: 0 },
289
+ });
290
+ const onSubmit = vi.fn();
291
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, onSubmit: onSubmit }));
292
+ stdin.write(prefix);
293
+ await flushInkStdin();
294
+ expect(lastFrame() ?? '').toContain(expectedSuggestion);
295
+ stdin.write(INK_TEST_KEYS.shiftDownArrow);
296
+ await flushInkStdin();
297
+ expect(lastFrame() ?? '').toContain('2/2');
298
+ expect(lastFrame() ?? '').not.toContain(`› ${expectedSuggestion}`);
299
+ stdin.write(INK_TEST_KEYS.shiftUpArrow);
300
+ await flushInkStdin();
301
+ expect(lastFrame() ?? '').toContain('1/2');
302
+ expect(lastFrame() ?? '').toContain(expectedSuggestion);
303
+ if (hasMultipleSuggestions) {
304
+ stdin.write(INK_TEST_KEYS.downArrow);
305
+ await flushInkStdin();
306
+ expect(lastFrame() ?? '').toContain('› secure: Secure mode');
307
+ }
308
+ stdin.write('\r');
309
+ await flushInkStdin();
310
+ expect(onSubmit).not.toHaveBeenCalled();
311
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}${completedValue}`);
312
+ expect(lastFrame() ?? '').not.toContain(`› ${completedValue}:`);
313
+ stdin.write(' because it is safer');
314
+ await flushInkStdin();
315
+ stdin.write('\r');
316
+ await flushInkStdin();
317
+ expect(onSubmit).toHaveBeenCalledWith(`${completedValue} because it is safer`, { targetOpenQuestionId: question.id });
318
+ });
319
+ it('prioritizes slash suggestions over answer suggestions in stream mode', async () => {
320
+ const viewModel = createTuiViewModel();
321
+ viewModel.setDisplayState({
322
+ workers: {},
323
+ conductorOutput: null,
324
+ openQuestions: [
325
+ createOpenQuestion({
326
+ id: 'inq-choice',
327
+ question: 'Choose a mode',
328
+ responseType: 'choice',
329
+ choices: [{ id: 'exit', label: 'Exit mode' }],
330
+ }),
331
+ ],
332
+ dispatchHold: { hold: false, heldEventCount: 0 },
333
+ });
334
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, onSubmit: () => { } }));
335
+ stdin.write('/');
336
+ await flushInkStdin();
337
+ expect(lastFrame() ?? '').toContain('› /reconnect');
338
+ expect(lastFrame() ?? '').not.toContain('› exit: Exit mode');
339
+ });
95
340
  it('renders activity as unframed static output and keeps the lower panes framed', () => {
96
341
  const viewModel = createTuiViewModel();
97
342
  viewModel.setDisplayState({
98
343
  workers: {
99
- implementer: { kind: 'implementer', status: 'running' },
344
+ implementer: {
345
+ kind: 'implementer',
346
+ status: 'running',
347
+ activity: 'calling: Shell',
348
+ },
100
349
  },
101
350
  conductorOutput: null,
102
351
  openQuestions: [createOpenQuestion({ id: 'inq-1', question: 'Approve?' })],
@@ -109,6 +358,8 @@ describe('IssueSessionTuiStream', () => {
109
358
  expect(frame).toContain('[operator] operator ping');
110
359
  expect(frame).toContain('[conductor] conductor says hi');
111
360
  expect(frame).toContain('Workers');
361
+ expect(frame).toContain('implementer: running (calling: Shell)');
362
+ expect(frame).not.toContain('implementer (implementer)');
112
363
  expect(frame).toContain('Open questions');
113
364
  expect(frame).toContain('Operator input');
114
365
  expect(frame).not.toContain('Orchestration');
@@ -117,6 +368,27 @@ describe('IssueSessionTuiStream', () => {
117
368
  expect(frame).toContain('inq-1 (1/1) への回答');
118
369
  expect(frame).not.toContain('任意入力');
119
370
  });
371
+ it('sizes the stream Workers pane for conductor and each worker row', () => {
372
+ const viewModel = createTuiViewModel();
373
+ viewModel.setDisplayState({
374
+ workers: {
375
+ conductor: { kind: 'conductor', status: 'idle' },
376
+ implementer: { kind: 'implementer', status: 'running' },
377
+ reviewer: { kind: 'reviewer', status: 'idle' },
378
+ },
379
+ conductorOutput: null,
380
+ openQuestions: [],
381
+ dispatchHold: { hold: false, heldEventCount: 0 },
382
+ });
383
+ const { lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, onSubmit: () => { } }));
384
+ const frame = lastFrame() ?? '';
385
+ const stats = extractTuiPaneFrameStats(frame, 'Workers');
386
+ expect(stats.contentRows).toHaveLength(3);
387
+ expect(stats.blankRows).toHaveLength(0);
388
+ expect(frame).toContain('conductor: idle');
389
+ expect(frame).toContain('implementer: running');
390
+ expect(frame).toContain('reviewer: idle');
391
+ });
120
392
  it('keeps operator line and paragraph breaks in stream activity output', () => {
121
393
  const viewModel = createTuiViewModel({ activityLogWindowSize: null });
122
394
  viewModel.appendActivityLog('operator', 'first line\n\nsecond line');
@@ -225,7 +497,7 @@ describe('IssueSessionTuiStream', () => {
225
497
  expect(frame).not.toContain('Open questions');
226
498
  expect(frame).not.toContain('(未回答なし)');
227
499
  expect(workersIndex).toBeGreaterThan(operatorInputIndex);
228
- expect(frame).toContain('operator>');
500
+ expect(frame).toContain(OPERATOR_INPUT_PROMPT);
229
501
  expect(frame).not.toContain('追加指示');
230
502
  expect(frame).not.toContain('/reconnect');
231
503
  expect(frame).toContain('otolab/agents-ensemble#261');
@@ -309,7 +581,7 @@ describe('IssueSessionTuiStream', () => {
309
581
  await flushInkStdin();
310
582
  expectCanonicalIssueUrl();
311
583
  const postLoopFrame = (lastFrame() ?? '').replace(/\s+/g, ' ');
312
- expect(postLoopFrame).toContain('operator>');
584
+ expect(postLoopFrame).toContain(OPERATOR_INPUT_PROMPT);
313
585
  expect(postLoopFrame).not.toContain('追加指示');
314
586
  expect(postLoopFrame).not.toContain('/reconnect');
315
587
  viewModel.setShuttingDown(true);
@@ -394,7 +666,7 @@ describe('IssueSessionTuiStream', () => {
394
666
  viewModel.setPostLoopWaiting(true);
395
667
  await flushInkStdin();
396
668
  expectCanonicalIssueLink();
397
- expect(lastFrame() ?? '').toContain('operator>');
669
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
398
670
  expect(lastFrame() ?? '').not.toContain('追加指示');
399
671
  expect(lastFrame() ?? '').not.toContain('/reconnect');
400
672
  viewModel.setShuttingDown(true);
@@ -406,7 +678,7 @@ describe('IssueSessionTuiStream', () => {
406
678
  const viewModel = createTuiViewModel();
407
679
  const { lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, issueUrl: "https://github.com/otolab/agents-ensemble/issues/261", issueLinkMode: "label", onSubmit: () => { } }));
408
680
  const frame = lastFrame() ?? '';
409
- expect(frame).toContain('operator>');
681
+ expect(frame).toContain(OPERATOR_INPUT_PROMPT);
410
682
  expect(frame).not.toContain('任意入力');
411
683
  expect(frame).not.toContain('/reconnect');
412
684
  expect(frame).not.toContain('自律ターン');
@@ -416,7 +688,7 @@ describe('IssueSessionTuiStream', () => {
416
688
  const onSubmit = vi.fn();
417
689
  const { stdin, lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, issueUrl: "https://github.com/otolab/agents-ensemble/issues/263", issueLinkMode: "label", onSubmit: onSubmit }));
418
690
  expect(lastFrame() ?? '').not.toContain('Open questions');
419
- expect(lastFrame() ?? '').toContain('operator>');
691
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
420
692
  expect(lastFrame() ?? '').not.toContain('任意入力');
421
693
  expect(lastFrame() ?? '').not.toContain('/reconnect');
422
694
  expect(lastFrame() ?? '').toContain('otolab/agents-ensemble#263');
@@ -427,16 +699,33 @@ describe('IssueSessionTuiStream', () => {
427
699
  await flushInkStdin();
428
700
  expect(onSubmit).toHaveBeenCalledWith('follow-up', undefined);
429
701
  });
430
- it('appends a later activity entry without replacing the earlier static entry', async () => {
702
+ it('separates changing activity labels in static output and groups same-label entries', async () => {
431
703
  const viewModel = createTuiViewModel();
432
704
  viewModel.appendActivityLog('harness', 'first');
433
705
  const { lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, onSubmit: () => { } }));
434
706
  expect(lastFrame() ?? '').toContain('[harness] first');
435
- viewModel.appendActivityLog('observation', 'second');
707
+ viewModel.appendActivityLog('harness', 'second');
708
+ viewModel.appendActivityLog('observation', 'third');
709
+ viewModel.appendActivityLog('operator', 'fourth');
710
+ viewModel.appendActivityLog('conductor', 'fifth');
436
711
  await flushInkStdin();
437
712
  const frame = lastFrame() ?? '';
713
+ const lines = frame.split('\n');
714
+ const harnessFirst = lines.findIndex((line) => line.includes('[harness] first'));
715
+ const harnessSecond = lines.findIndex((line) => line.includes('[harness] second'));
716
+ const observation = lines.findIndex((line) => line.includes('[observation] third'));
717
+ const operator = lines.findIndex((line) => line.includes('[operator] fourth'));
718
+ const conductor = lines.findIndex((line) => line.includes('[conductor] fifth'));
719
+ expect(harnessFirst).toBeGreaterThanOrEqual(0);
720
+ expect(harnessSecond).toBe(harnessFirst + 1);
721
+ expect(observation).toBe(harnessSecond + 2);
722
+ expect(operator).toBe(observation + 2);
723
+ expect(conductor).toBe(operator + 2);
438
724
  expect(frame).toContain('[harness] first');
439
- expect(frame).toContain('[observation] second');
725
+ expect(frame).toContain('[harness] second');
726
+ expect(frame).toContain('[observation] third');
727
+ expect(frame).toContain('[operator] fourth');
728
+ expect(frame).toContain('[conductor] fifth');
440
729
  });
441
730
  it('holds new activity while detached and flushes the pending suffix once on End', async () => {
442
731
  const viewModel = createTuiViewModel({ activityLogWindowSize: null });
@@ -545,7 +834,7 @@ describe('IssueSessionTuiStream', () => {
545
834
  expect(countHistory(finalFrame)).toBe(1);
546
835
  expect(Math.max(...finalFrame.split('\n').map((line) => line.trimEnd().length))).toBeLessThanOrEqual(60);
547
836
  expect(finalFrame).toContain('Workers');
548
- expect(finalFrame).toContain('operator>');
837
+ expect(finalFrame).toContain(OPERATOR_INPUT_PROMPT);
549
838
  }
550
839
  finally {
551
840
  unsubscribe();
@@ -738,7 +1027,7 @@ describe('IssueSessionTuiStream', () => {
738
1027
  });
739
1028
  if (restoredQuestions.length === 1) {
740
1029
  expect(lastFrame() ?? '').not.toContain('Open questions');
741
- expect(lastFrame() ?? '').toContain('operator>');
1030
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
742
1031
  expect(lastFrame() ?? '').not.toContain('任意入力');
743
1032
  }
744
1033
  else {
@@ -748,7 +1037,7 @@ describe('IssueSessionTuiStream', () => {
748
1037
  setOperatorQuestions([]);
749
1038
  await flushInkStdin();
750
1039
  expect(lastFrame() ?? '').not.toContain('Open questions');
751
- expect(lastFrame() ?? '').toContain('operator>');
1040
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
752
1041
  expect(lastFrame() ?? '').not.toContain('任意入力');
753
1042
  }
754
1043
  });
@@ -772,5 +1061,72 @@ describe('IssueSessionTuiStream', () => {
772
1061
  expect(lastFrame() ?? '').toContain('2/2');
773
1062
  expect(lastFrame() ?? '').toContain('▸ inq-2');
774
1063
  });
1064
+ it('scrolls the selected full context from top to bottom without taking suggestion arrows', async () => {
1065
+ const viewModel = createTuiViewModel();
1066
+ viewModel.setDisplayState({
1067
+ workers: {},
1068
+ conductorOutput: null,
1069
+ openQuestions: [
1070
+ createOpenQuestion({
1071
+ id: 'inq-long',
1072
+ question: 'Choose a mode',
1073
+ responseType: 'choice',
1074
+ context: LONG_QUESTION_CONTEXT,
1075
+ choices: [
1076
+ { id: 'safe', label: 'Safe mode' },
1077
+ { id: 'secure', label: 'Secure mode' },
1078
+ ],
1079
+ }),
1080
+ createOpenQuestion({
1081
+ id: 'inq-next',
1082
+ question: 'Anything else?',
1083
+ }),
1084
+ ],
1085
+ dispatchHold: { hold: false, heldEventCount: 0 },
1086
+ });
1087
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTuiStream, { viewModel: viewModel, onSubmit: () => { } }));
1088
+ const initialFrame = lastFrame() ?? '';
1089
+ expect(initialFrame).toContain('context-line-00');
1090
+ expect(initialFrame).not.toContain('context-line-39');
1091
+ expect(initialFrame).toContain('Shift+PgUp/PgDnで本文スクロール');
1092
+ expect(initialFrame).toContain('で選択) · Shift+PgUp/PgDn');
1093
+ expect(initialFrame).toContain('› safe: Safe mode');
1094
+ stdin.write(INK_TEST_KEYS.shiftPageDown.repeat(2));
1095
+ await flushInkStdin();
1096
+ const scrolledFrame = lastFrame() ?? '';
1097
+ expect(scrolledFrame).not.toContain('context-line-00');
1098
+ expect(scrolledFrame).toContain('context-line-01');
1099
+ expect(scrolledFrame).toContain('› safe: Safe mode');
1100
+ stdin.write(INK_TEST_KEYS.downArrow);
1101
+ await flushInkStdin();
1102
+ expect(lastFrame() ?? '').toContain('› secure: Secure mode');
1103
+ expect(lastFrame() ?? '').toContain('context-line-01');
1104
+ stdin.write('/');
1105
+ await flushInkStdin();
1106
+ expect(lastFrame() ?? '').toContain('› /reconnect');
1107
+ stdin.write(INK_TEST_KEYS.shiftPageDown.repeat(200));
1108
+ await flushInkStdin();
1109
+ const bottomFrame = lastFrame() ?? '';
1110
+ expect(bottomFrame).toContain('context-line-39');
1111
+ expect(bottomFrame).not.toContain('context-line-00');
1112
+ expect(bottomFrame).toContain('› /reconnect');
1113
+ stdin.write(INK_TEST_KEYS.shiftDownArrow);
1114
+ await flushInkStdin();
1115
+ expect(lastFrame() ?? '').toContain('2/2');
1116
+ stdin.write(INK_TEST_KEYS.shiftUpArrow);
1117
+ await flushInkStdin();
1118
+ const resetFrame = lastFrame() ?? '';
1119
+ expect(resetFrame).toContain('1/2');
1120
+ expect(resetFrame).toContain('context-line-00');
1121
+ expect(resetFrame).not.toContain('context-line-39');
1122
+ stdin.write(INK_TEST_KEYS.shiftPageDown.repeat(200));
1123
+ await flushInkStdin();
1124
+ expect(lastFrame() ?? '').toContain('context-line-39');
1125
+ stdin.write(INK_TEST_KEYS.shiftPageUp.repeat(200));
1126
+ await flushInkStdin();
1127
+ const topFrame = lastFrame() ?? '';
1128
+ expect(topFrame).toContain('context-line-00');
1129
+ expect(topFrame).not.toContain('context-line-39');
1130
+ });
775
1131
  });
776
1132
  //# sourceMappingURL=issue-session-tui-stream.test.js.map