@agents-ensemble/cli 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +3 -0
  2. package/dist/format-session-summary.d.ts.map +1 -1
  3. package/dist/format-session-summary.js +74 -7
  4. package/dist/format-session-summary.js.map +1 -1
  5. package/dist/index.js +14 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/tui/format-operator-context.d.ts +2 -3
  8. package/dist/tui/format-operator-context.d.ts.map +1 -1
  9. package/dist/tui/format-operator-context.js +6 -12
  10. package/dist/tui/format-operator-context.js.map +1 -1
  11. package/dist/tui/ink-test-keys.d.ts +1 -0
  12. package/dist/tui/ink-test-keys.d.ts.map +1 -1
  13. package/dist/tui/ink-test-keys.js +1 -0
  14. package/dist/tui/ink-test-keys.js.map +1 -1
  15. package/dist/tui/issue-session-tui-stream-cursor.test.js +58 -11
  16. package/dist/tui/issue-session-tui-stream-cursor.test.js.map +1 -1
  17. package/dist/tui/issue-session-tui-stream.d.ts.map +1 -1
  18. package/dist/tui/issue-session-tui-stream.js +86 -7
  19. package/dist/tui/issue-session-tui-stream.js.map +1 -1
  20. package/dist/tui/issue-session-tui-stream.test.js +147 -21
  21. package/dist/tui/issue-session-tui-stream.test.js.map +1 -1
  22. package/dist/tui/issue-session-tui.d.ts.map +1 -1
  23. package/dist/tui/issue-session-tui.js +90 -9
  24. package/dist/tui/issue-session-tui.js.map +1 -1
  25. package/dist/tui/issue-session-tui.test.js +280 -54
  26. package/dist/tui/issue-session-tui.test.js.map +1 -1
  27. package/dist/tui/open-question-answer-suggestions.d.ts +19 -0
  28. package/dist/tui/open-question-answer-suggestions.d.ts.map +1 -0
  29. package/dist/tui/open-question-answer-suggestions.js +62 -0
  30. package/dist/tui/open-question-answer-suggestions.js.map +1 -0
  31. package/dist/tui/operator-command-suggestions.d.ts +13 -1
  32. package/dist/tui/operator-command-suggestions.d.ts.map +1 -1
  33. package/dist/tui/operator-command-suggestions.js +23 -2
  34. package/dist/tui/operator-command-suggestions.js.map +1 -1
  35. package/dist/tui/operator-text-area.d.ts +9 -2
  36. package/dist/tui/operator-text-area.d.ts.map +1 -1
  37. package/dist/tui/operator-text-area.js +5 -2
  38. package/dist/tui/operator-text-area.js.map +1 -1
  39. package/dist/tui/operator-text-area.test-double.d.ts +1 -1
  40. package/dist/tui/operator-text-area.test-double.d.ts.map +1 -1
  41. package/dist/tui/operator-text-area.test-double.js +87 -6
  42. package/dist/tui/operator-text-area.test-double.js.map +1 -1
  43. package/dist/tui/operator-text-area.test-stub.d.ts +5 -1
  44. package/dist/tui/operator-text-area.test-stub.d.ts.map +1 -1
  45. package/dist/tui/operator-text-area.test-stub.js +6 -2
  46. package/dist/tui/operator-text-area.test-stub.js.map +1 -1
  47. package/dist/tui/operator-text-area.test.js +13 -0
  48. package/dist/tui/operator-text-area.test.js.map +1 -1
  49. package/dist/tui/tui-layout-constants.d.ts +3 -5
  50. package/dist/tui/tui-layout-constants.d.ts.map +1 -1
  51. package/dist/tui/tui-layout-constants.js +3 -5
  52. package/dist/tui/tui-layout-constants.js.map +1 -1
  53. package/dist/tui/worker-display-name.d.ts +2 -0
  54. package/dist/tui/worker-display-name.d.ts.map +1 -0
  55. package/dist/tui/worker-display-name.js +4 -0
  56. package/dist/tui/worker-display-name.js.map +1 -0
  57. package/package.json +3 -2
@@ -6,14 +6,14 @@ import { cleanup, render } from 'ink-testing-library';
6
6
  import { IssueSessionTui } from './issue-session-tui.js';
7
7
  import { createTuiViewModel } from './tui-view-model.js';
8
8
  import { buildActivityLogDisplayLines } from './activity-log.js';
9
- import { formatOperatorContextHint } from './format-operator-context.js';
9
+ import { resolveOperatorInputDisplayMode } from './format-operator-context.js';
10
10
  import { flushInkStdin, INK_TEST_KEYS } from './ink-test-keys.js';
11
11
  import { extractOrchestrationPaneFrameStats, extractTuiPaneFrameStats } from './orchestration-pane-frame.js';
12
12
  import { resolveOpenQuestionsPaneLayout } from './open-questions-pane.js';
13
13
  import { computeActivityLogLineCapacity, computeActivityPaneHeight, computeInputPaneHeight, computeOrchestrationLogVisibleLineCount, computeOperatorInputCursorX, computeOperatorInputLineIndex, } from './compute-operator-input-cursor-y.js';
14
- import { getPaneContentWidth, wrapTextToWidth } from './wrap-text-to-width.js';
14
+ import { getPaneContentWidth } from './wrap-text-to-width.js';
15
15
  import { resolvePaneHeights } from './pane-layout.js';
16
- import { INPUT_PANE_TITLE, MAIN_PANE_TITLE, OPEN_QUESTIONS_PANE_MIN_HEIGHT, OPERATOR_INPUT_POST_LOOP_HINT, OPERATOR_INPUT_CURSOR_Y_OFFSET, PANE_PADDING_X, ROUND_BORDER_WIDTH, WORKER_PANE_HEIGHT, WORKER_PANE_TITLE, } from './tui-layout-constants.js';
16
+ 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_HEIGHT, WORKER_PANE_TITLE, } from './tui-layout-constants.js';
17
17
  import { createTuiResizeController, createTuiTerminalSizeStore, TUI_RESIZE_SETTLE_MS, TUI_RESIZE_SHRINK_COALESCE_MS, } from './tui-terminal-size.js';
18
18
  class ResizeSource extends EventEmitter {
19
19
  columns = 80;
@@ -21,12 +21,12 @@ class ResizeSource extends EventEmitter {
21
21
  }
22
22
  function findOperatorInputLine(lines) {
23
23
  const operatorLineIndices = lines
24
- .map((line, index) => (line.includes('operator>') ? index : -1))
24
+ .map((line, index) => (line.includes(OPERATOR_INPUT_PROMPT) ? index : -1))
25
25
  .filter((index) => index >= 0);
26
26
  const lineIndex = operatorLineIndices.at(-1) ?? -1;
27
27
  const line = lines[lineIndex] ?? '';
28
- const promptIndex = line.lastIndexOf('operator> ');
29
- const inputStartX = promptIndex >= 0 ? promptIndex + 'operator> '.length : -1;
28
+ const promptIndex = line.lastIndexOf(OPERATOR_INPUT_PROMPT);
29
+ const inputStartX = promptIndex >= 0 ? promptIndex + OPERATOR_INPUT_PROMPT.length : -1;
30
30
  return { lineIndex, inputStartX };
31
31
  }
32
32
  function expectNoContentOnBorderLines(frame) {
@@ -34,6 +34,7 @@ function expectNoContentOnBorderLines(frame) {
34
34
  if (line.startsWith('╭') || line.startsWith('╰') || line.startsWith('┌') || line.startsWith('└')) {
35
35
  expect(line).not.toMatch(/\[(?:operator|conductor|implementer|reviewer)\]/);
36
36
  expect(line).not.toMatch(/operator>/);
37
+ expect(line).not.toContain(OPERATOR_INPUT_PROMPT);
37
38
  }
38
39
  }
39
40
  }
@@ -96,17 +97,214 @@ describe('IssueSessionTui', () => {
96
97
  stdin.write('/');
97
98
  await flushInkStdin();
98
99
  const allCommandsFrame = lastFrame() ?? '';
100
+ expect(allCommandsFrame).toContain(`${OPERATOR_INPUT_PROMPT}/`);
101
+ expect(allCommandsFrame).not.toContain('operator> ');
99
102
  expect(allCommandsFrame).toContain('/reconnect — conductor を再接続');
100
103
  expect(allCommandsFrame).toContain('/exit — セッションを終了');
101
104
  stdin.write('ex');
102
105
  await flushInkStdin();
103
106
  const filteredFrame = lastFrame() ?? '';
107
+ expect(filteredFrame).toContain(`${OPERATOR_INPUT_PROMPT}/ex`);
104
108
  expect(filteredFrame).toContain('/exit — セッションを終了');
105
109
  expect(filteredFrame).not.toContain('/reconnect — conductor を再接続');
106
110
  stdin.write('\r');
107
111
  await flushInkStdin();
108
112
  expect(onSubmit).toHaveBeenCalledWith('/ex', undefined);
109
113
  });
114
+ it('sizes the input pane using the shortened prompt width', async () => {
115
+ Object.defineProperty(process.stdout, 'columns', {
116
+ configurable: true,
117
+ value: 20,
118
+ });
119
+ const viewModel = createTuiViewModel();
120
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
121
+ stdin.write('1234567890');
122
+ await flushInkStdin();
123
+ const frame = lastFrame() ?? '';
124
+ expect(frame).toContain(`${OPERATOR_INPUT_PROMPT}1234567890`);
125
+ expect(frame).not.toContain('operator> ');
126
+ });
127
+ it('selects slash suggestions with bounded arrows and completes with Tab before Enter submits', async () => {
128
+ const viewModel = createTuiViewModel();
129
+ const onSubmit = vi.fn();
130
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: onSubmit }));
131
+ stdin.write('/');
132
+ await flushInkStdin();
133
+ expect(lastFrame() ?? '').toContain('› /reconnect');
134
+ for (const key of [
135
+ INK_TEST_KEYS.downArrow,
136
+ INK_TEST_KEYS.downArrow,
137
+ ]) {
138
+ stdin.write(key);
139
+ await flushInkStdin();
140
+ }
141
+ expect(lastFrame() ?? '').toContain('› /close');
142
+ stdin.write(INK_TEST_KEYS.downArrow);
143
+ await flushInkStdin();
144
+ expect(lastFrame() ?? '').toContain('› /close');
145
+ for (const key of [INK_TEST_KEYS.upArrow, INK_TEST_KEYS.upArrow]) {
146
+ stdin.write(key);
147
+ await flushInkStdin();
148
+ }
149
+ expect(lastFrame() ?? '').toContain('› /reconnect');
150
+ stdin.write(INK_TEST_KEYS.upArrow);
151
+ await flushInkStdin();
152
+ expect(lastFrame() ?? '').toContain('› /reconnect');
153
+ for (const key of [
154
+ INK_TEST_KEYS.downArrow,
155
+ INK_TEST_KEYS.downArrow,
156
+ INK_TEST_KEYS.tab,
157
+ ]) {
158
+ stdin.write(key);
159
+ await flushInkStdin();
160
+ }
161
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}/close`);
162
+ expect(lastFrame() ?? '').not.toContain('operator> ');
163
+ expect(onSubmit).not.toHaveBeenCalled();
164
+ stdin.write('\r');
165
+ await flushInkStdin();
166
+ expect(onSubmit).toHaveBeenCalledWith('/close', undefined);
167
+ });
168
+ it.each([
169
+ {
170
+ name: 'choice',
171
+ question: createOpenQuestion({
172
+ id: 'inq-choice',
173
+ question: 'Choose a mode',
174
+ responseType: 'choice',
175
+ choices: [
176
+ { id: 'safe', label: 'Safe mode' },
177
+ { id: 'secure', label: 'Secure mode' },
178
+ { id: 'fast', label: 'Fast mode' },
179
+ ],
180
+ }),
181
+ prefix: 's',
182
+ completedValue: 'secure',
183
+ expectedSuggestion: 'safe: Safe mode',
184
+ hasMultipleSuggestions: true,
185
+ },
186
+ {
187
+ name: 'yes_no',
188
+ question: createOpenQuestion({
189
+ id: 'inq-yes-no',
190
+ question: 'Proceed?',
191
+ responseType: 'yes_no',
192
+ }),
193
+ prefix: 'n',
194
+ completedValue: 'no',
195
+ expectedSuggestion: 'no — いいえ',
196
+ hasMultipleSuggestions: false,
197
+ },
198
+ ])('completes and submits the selected $name answer in pane mode', async ({ question, prefix, completedValue, expectedSuggestion, hasMultipleSuggestions, }) => {
199
+ const viewModel = createTuiViewModel();
200
+ viewModel.setDisplayState({
201
+ workers: {},
202
+ conductorOutput: null,
203
+ openQuestions: [
204
+ question,
205
+ createOpenQuestion({
206
+ id: 'inq-text',
207
+ question: 'Anything else?',
208
+ }),
209
+ ],
210
+ dispatchHold: { hold: false, heldEventCount: 0 },
211
+ });
212
+ const onSubmit = vi.fn();
213
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: onSubmit }));
214
+ stdin.write(prefix);
215
+ await flushInkStdin();
216
+ expect(lastFrame() ?? '').toContain(expectedSuggestion);
217
+ stdin.write(INK_TEST_KEYS.shiftDownArrow);
218
+ await flushInkStdin();
219
+ expect(lastFrame() ?? '').toContain('2/2');
220
+ expect(lastFrame() ?? '').not.toContain(`› ${expectedSuggestion}`);
221
+ stdin.write(INK_TEST_KEYS.shiftUpArrow);
222
+ await flushInkStdin();
223
+ expect(lastFrame() ?? '').toContain('1/2');
224
+ expect(lastFrame() ?? '').toContain(expectedSuggestion);
225
+ if (hasMultipleSuggestions) {
226
+ stdin.write(INK_TEST_KEYS.downArrow);
227
+ await flushInkStdin();
228
+ expect(lastFrame() ?? '').toContain('› secure: Secure mode');
229
+ }
230
+ stdin.write('\r');
231
+ await flushInkStdin();
232
+ expect(onSubmit).not.toHaveBeenCalled();
233
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}${completedValue}`);
234
+ expect(lastFrame() ?? '').not.toContain(`› ${completedValue}:`);
235
+ stdin.write(' because it is safer');
236
+ await flushInkStdin();
237
+ stdin.write('\r');
238
+ await flushInkStdin();
239
+ expect(onSubmit).toHaveBeenCalledWith(`${completedValue} because it is safer`, { targetOpenQuestionId: question.id });
240
+ });
241
+ it('prioritizes slash suggestions over answer suggestions in pane mode', async () => {
242
+ const viewModel = createTuiViewModel();
243
+ viewModel.setDisplayState({
244
+ workers: {},
245
+ conductorOutput: null,
246
+ openQuestions: [
247
+ createOpenQuestion({
248
+ id: 'inq-choice',
249
+ question: 'Choose a mode',
250
+ responseType: 'choice',
251
+ choices: [{ id: 'exit', label: 'Exit mode' }],
252
+ }),
253
+ ],
254
+ dispatchHold: { hold: false, heldEventCount: 0 },
255
+ });
256
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
257
+ stdin.write('/');
258
+ await flushInkStdin();
259
+ expect(lastFrame() ?? '').toContain('› /reconnect');
260
+ expect(lastFrame() ?? '').not.toContain('› exit: Exit mode');
261
+ });
262
+ it('resets selection to the first matching candidate when the prefix changes', async () => {
263
+ const viewModel = createTuiViewModel();
264
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
265
+ stdin.write('/');
266
+ await flushInkStdin();
267
+ stdin.write(INK_TEST_KEYS.downArrow);
268
+ await flushInkStdin();
269
+ stdin.write(INK_TEST_KEYS.downArrow);
270
+ await flushInkStdin();
271
+ expect(lastFrame() ?? '').toContain('› /close');
272
+ stdin.write('e');
273
+ await flushInkStdin();
274
+ expect(lastFrame() ?? '').toContain('› /exit');
275
+ stdin.write(INK_TEST_KEYS.tab);
276
+ await flushInkStdin();
277
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}/exit`);
278
+ });
279
+ it('keeps Tab and arrow editing behavior when there is no match or input is multiline', async () => {
280
+ const viewModel = createTuiViewModel();
281
+ const onSubmit = vi.fn();
282
+ const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: onSubmit }));
283
+ stdin.write('/unknown');
284
+ await flushInkStdin();
285
+ stdin.write(INK_TEST_KEYS.tab);
286
+ await flushInkStdin();
287
+ expect(lastFrame() ?? '').toContain(`${OPERATOR_INPUT_PROMPT}/unknown`);
288
+ expect(onSubmit).not.toHaveBeenCalled();
289
+ stdin.write('\r');
290
+ await flushInkStdin();
291
+ expect(onSubmit).toHaveBeenCalledWith('/unknown', undefined);
292
+ cleanup();
293
+ const secondViewModel = createTuiViewModel();
294
+ const multilineSubmit = vi.fn();
295
+ const multiline = render(_jsx(IssueSessionTui, { viewModel: secondViewModel, onSubmit: multilineSubmit }));
296
+ multiline.stdin.write('\u001B[200~first\nsecond\u001B[201~');
297
+ await flushInkStdin();
298
+ multiline.stdin.write(INK_TEST_KEYS.upArrow);
299
+ await flushInkStdin();
300
+ multiline.stdin.write('X');
301
+ await flushInkStdin();
302
+ expect(multiline.lastFrame() ?? '').toContain('firstX');
303
+ expect(multiline.lastFrame() ?? '').not.toContain('› /');
304
+ multiline.stdin.write('\r');
305
+ await flushInkStdin();
306
+ expect(multilineSubmit).toHaveBeenCalledWith('firstX\nsecond', undefined);
307
+ });
110
308
  it('renders four panes with worker, session log, open questions, and input', () => {
111
309
  const viewModel = createTuiViewModel();
112
310
  viewModel.setDisplayState({
@@ -139,7 +337,7 @@ describe('IssueSessionTui', () => {
139
337
  expect(frame).toContain('inq-1');
140
338
  expect(frame).toContain('1/1');
141
339
  expect(frame).toContain('Shift+↑↓で選択');
142
- expect(frame).toContain('operator>');
340
+ expect(frame).toContain(OPERATOR_INPUT_PROMPT);
143
341
  expect(frame).toContain(INPUT_PANE_TITLE);
144
342
  });
145
343
  it('shows dispatch hold count in the Workers pane title', () => {
@@ -164,12 +362,14 @@ describe('IssueSessionTui', () => {
164
362
  const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
165
363
  expect(lastFrame() ?? '').toContain('conductor dispatch 保留中(1 件)');
166
364
  });
167
- it('shows post-loop prompt in input area without waiting status', () => {
365
+ it('keeps the post-loop input area free of a fixed prompt', () => {
168
366
  const viewModel = createTuiViewModel();
169
367
  viewModel.setPostLoopWaiting(true);
170
368
  const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
171
369
  const frame = lastFrame() ?? '';
172
- expect(frame).toContain('追加指示 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
370
+ expect(frame).toContain(OPERATOR_INPUT_PROMPT);
371
+ expect(frame).not.toContain('追加指示');
372
+ expect(frame).not.toContain('/reconnect');
173
373
  expect(frame).not.toContain('post-loop 待機中');
174
374
  });
175
375
  it('shows the issue link in the workers pane header and prompt-only operator input', async () => {
@@ -188,8 +388,9 @@ describe('IssueSessionTui', () => {
188
388
  expect(frame).toContain(issueLabel);
189
389
  expect(frame).toContain(osc8Open);
190
390
  expect(frame).not.toContain(nonCanonicalOsc8Open);
191
- expect(frame).toContain('任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
192
- expect(frame).not.toContain(`${issueLabel} — 任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了`);
391
+ expect(frame).toContain(OPERATOR_INPUT_PROMPT);
392
+ expect(frame).not.toContain('任意入力');
393
+ expect(frame).not.toContain('/reconnect');
193
394
  viewModel.setDisplayState({
194
395
  workers: {},
195
396
  conductorOutput: null,
@@ -224,7 +425,9 @@ describe('IssueSessionTui', () => {
224
425
  expect(lastFrame() ?? '').toContain(issueLabel);
225
426
  expect(lastFrame() ?? '').toContain(osc8Open);
226
427
  expect(lastFrame() ?? '').not.toContain(nonCanonicalOsc8Open);
227
- expect(lastFrame() ?? '').toContain('追加指示 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
428
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
429
+ expect(lastFrame() ?? '').not.toContain('追加指示');
430
+ expect(lastFrame() ?? '').not.toContain('/reconnect');
228
431
  expect(lastFrame() ?? '').not.toContain('post-loop 待機中');
229
432
  viewModel.setShuttingDown(true);
230
433
  await flushInkStdin();
@@ -316,11 +519,9 @@ describe('IssueSessionTui', () => {
316
519
  await flushInkStdin();
317
520
  expectCanonicalIssueUrl();
318
521
  const postLoopFrame = (lastFrame() ?? '').replace(/\s+/g, ' ');
319
- expect(postLoopFrame).toContain('追加指示 ·');
320
- expect(postLoopFrame).toContain('/close');
321
- expect(postLoopFrame).toContain('保存して閉じる');
322
- expect(postLoopFrame).toContain('/exit');
323
- expect(postLoopFrame).toContain('掃除して終了');
522
+ expect(postLoopFrame).toContain(OPERATOR_INPUT_PROMPT);
523
+ expect(postLoopFrame).not.toContain('追加指示');
524
+ expect(postLoopFrame).not.toContain('/reconnect');
324
525
  viewModel.setShuttingDown(true);
325
526
  await flushInkStdin();
326
527
  expectCanonicalIssueUrl();
@@ -399,14 +600,11 @@ describe('IssueSessionTui', () => {
399
600
  maxTurns: null,
400
601
  openQuestions: [],
401
602
  });
402
- const operatorPrompt = 'operator> ';
403
- const contentWidth = getPaneContentWidth({
404
- columns: terminalColumns,
405
- paddingX: PANE_PADDING_X,
406
- borderWidth: ROUND_BORDER_WIDTH,
407
- });
408
- const contextHint = formatOperatorContextHint(viewModel.getSnapshot().operatorContext);
409
- const hintLineCount = wrapTextToWidth(contextHint, contentWidth).length;
603
+ const operatorPrompt = OPERATOR_INPUT_PROMPT;
604
+ const hintLineCount = resolveOperatorInputDisplayMode({
605
+ openQuestions: viewModel.getSnapshot().operatorContext?.openQuestions ?? [],
606
+ }).hintLines.length;
607
+ expect(hintLineCount).toBe(0);
410
608
  const openQuestionsPaneHeight = 0;
411
609
  const expectedInputLineIndex = computeOperatorInputLineIndex({
412
610
  terminalRows: liveFrameRows,
@@ -424,7 +622,7 @@ describe('IssueSessionTui', () => {
424
622
  expect(expectedCursorY).toBe(lineIndex + OPERATOR_INPUT_CURSOR_Y_OFFSET);
425
623
  expect(lines).toHaveLength(liveFrameRows);
426
624
  });
427
- it('aligns IME cursor coordinates when context hint wraps on a narrow terminal', () => {
625
+ it('aligns IME cursor coordinates without a fixed hint on a narrow terminal', () => {
428
626
  const terminalRows = 24;
429
627
  const liveFrameRows = terminalRows - 1;
430
628
  const terminalColumns = 40;
@@ -443,14 +641,11 @@ describe('IssueSessionTui', () => {
443
641
  maxTurns: null,
444
642
  openQuestions: [],
445
643
  });
446
- const operatorPrompt = 'operator> ';
447
- const contentWidth = getPaneContentWidth({
448
- columns: terminalColumns,
449
- paddingX: PANE_PADDING_X,
450
- borderWidth: ROUND_BORDER_WIDTH,
451
- });
452
- const contextHint = formatOperatorContextHint(viewModel.getSnapshot().operatorContext);
453
- const hintLineCount = wrapTextToWidth(contextHint, contentWidth).length;
644
+ const operatorPrompt = OPERATOR_INPUT_PROMPT;
645
+ const hintLineCount = resolveOperatorInputDisplayMode({
646
+ openQuestions: viewModel.getSnapshot().operatorContext?.openQuestions ?? [],
647
+ }).hintLines.length;
648
+ expect(hintLineCount).toBe(0);
454
649
  const openQuestionsPaneHeight = 0;
455
650
  const expectedInputLineIndex = computeOperatorInputLineIndex({
456
651
  terminalRows: liveFrameRows,
@@ -462,7 +657,7 @@ describe('IssueSessionTui', () => {
462
657
  const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
463
658
  const lines = (lastFrame() ?? '').split('\n');
464
659
  const { lineIndex, inputStartX } = findOperatorInputLine(lines);
465
- expect(hintLineCount).toBeGreaterThan(1);
660
+ expect(hintLineCount).toBe(0);
466
661
  expect(lineIndex).toBeGreaterThanOrEqual(0);
467
662
  expect(inputStartX).toBe(expectedCursorX);
468
663
  expect(lineIndex).toBe(expectedInputLineIndex);
@@ -482,6 +677,8 @@ describe('IssueSessionTui', () => {
482
677
  });
483
678
  const { lastFrame: thinkingFrame, unmount: unmountThinking } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
484
679
  expect(thinkingFrame() ?? '').toContain('conductor: thinking');
680
+ expect(thinkingFrame() ?? '').toContain('implementer: idle');
681
+ expect(thinkingFrame() ?? '').not.toContain('implementer (implementer)');
485
682
  unmountThinking();
486
683
  viewModel.setDisplayState({
487
684
  workers: {
@@ -496,6 +693,33 @@ describe('IssueSessionTui', () => {
496
693
  expect(idleFrame() ?? '').toContain('conductor: idle');
497
694
  expect(idleFrame() ?? '').not.toContain('conductor: thinking');
498
695
  });
696
+ it('formats worker names and running activity in the workers pane', () => {
697
+ const viewModel = createTuiViewModel();
698
+ viewModel.setDisplayState({
699
+ workers: {
700
+ conductor: { kind: 'conductor', status: 'running' },
701
+ implementer: {
702
+ kind: 'implementer',
703
+ status: 'running',
704
+ activity: 'calling: Shell',
705
+ },
706
+ impl: {
707
+ kind: 'implementer',
708
+ status: 'running',
709
+ activity: 'editing files',
710
+ },
711
+ },
712
+ conductorOutput: null,
713
+ openQuestions: [],
714
+ dispatchHold: { hold: false, heldEventCount: 0 },
715
+ });
716
+ const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
717
+ const frame = lastFrame() ?? '';
718
+ expect(frame).toContain('conductor: thinking');
719
+ expect(frame).toContain('implementer: running (calling: Shell)');
720
+ expect(frame).not.toContain('implementer (implementer)');
721
+ expect(frame).toContain('impl (implementer): running (editing files)');
722
+ });
499
723
  it('does not bleed activity log text onto pane border lines', () => {
500
724
  const terminalRows = 24;
501
725
  const liveFrameRows = terminalRows - 1;
@@ -557,7 +781,7 @@ describe('IssueSessionTui', () => {
557
781
  const frame = lastFrame() ?? '';
558
782
  expect(frame.split('\n')).toHaveLength(11);
559
783
  expect(Math.max(...frame.split('\n').map((line) => line.trimEnd().length))).toBeLessThanOrEqual(40);
560
- expect(frame).toContain('operator>');
784
+ expect(frame).toContain(OPERATOR_INPUT_PROMPT);
561
785
  terminalSizeStore.dispose();
562
786
  });
563
787
  it('coalesces staged same-row shrink before redrawing the pane frame', async () => {
@@ -602,7 +826,7 @@ describe('IssueSessionTui', () => {
602
826
  expect(finalFrame).not.toBe(initialFrame);
603
827
  expect(Math.max(...finalFrame.split('\n').map((line) => line.trimEnd().length))).toBeLessThanOrEqual(60);
604
828
  expect(finalFrame).toContain(WORKER_PANE_TITLE);
605
- expect(finalFrame).toContain('operator>');
829
+ expect(finalFrame).toContain(OPERATOR_INPUT_PROMPT);
606
830
  }
607
831
  finally {
608
832
  unsubscribe();
@@ -632,12 +856,7 @@ describe('IssueSessionTui', () => {
632
856
  const stats = extractTuiPaneFrameStats(frame, title);
633
857
  expect(stats.titleOnBorder).toBe(true);
634
858
  }
635
- const contentWidth = getPaneContentWidth({
636
- columns: 60,
637
- paddingX: PANE_PADDING_X,
638
- borderWidth: ROUND_BORDER_WIDTH,
639
- });
640
- const hintLineCount = wrapTextToWidth(OPERATOR_INPUT_POST_LOOP_HINT, contentWidth).length;
859
+ const hintLineCount = 0;
641
860
  const requestedInputPaneHeight = computeInputPaneHeight({
642
861
  hintLineCount,
643
862
  inputDisplayLineCount: 1,
@@ -649,10 +868,10 @@ describe('IssueSessionTui', () => {
649
868
  inputPaneHeight: requestedInputPaneHeight,
650
869
  });
651
870
  expect(paneHeights).toEqual({
652
- workerPaneHeight: 3,
871
+ workerPaneHeight: 5,
653
872
  activityPaneHeight: 3,
654
873
  openQuestionsPaneHeight: 0,
655
- inputPaneHeight: 5,
874
+ inputPaneHeight: 3,
656
875
  });
657
876
  const inputLine = findOperatorInputLine(lines);
658
877
  expect(inputLine.lineIndex).toBe(computeOperatorInputLineIndex({
@@ -667,7 +886,8 @@ describe('IssueSessionTui', () => {
667
886
  expect(inputLine.lineIndex).toBeLessThan(lines.length);
668
887
  expect(inputLine.inputStartX).toBeGreaterThanOrEqual(0);
669
888
  expect(frame).toContain('Orchestration');
670
- expect(frame).toContain('operator>');
889
+ expect(frame).toContain(OPERATOR_INPUT_PROMPT);
890
+ expect(frame).not.toContain('operator> ');
671
891
  }
672
892
  finally {
673
893
  terminalSizeStore.dispose();
@@ -681,13 +901,16 @@ describe('IssueSessionTui', () => {
681
901
  expect(frame).toContain('(活動ログなし)');
682
902
  expect(frame).not.toContain('Open questions');
683
903
  expect(frame).not.toContain('(未回答なし)');
684
- expect(frame).toContain('任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
904
+ expect(frame).toContain(OPERATOR_INPUT_PROMPT);
905
+ expect(frame).not.toContain('任意入力');
906
+ expect(frame).not.toContain('/reconnect');
685
907
  });
686
908
  it('switches the lower UI between no-question and question modes', async () => {
687
909
  const viewModel = createTuiViewModel();
688
910
  const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
689
911
  expect(lastFrame() ?? '').not.toContain('Open questions');
690
- expect(lastFrame() ?? '').toContain('任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
912
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
913
+ expect(lastFrame() ?? '').not.toContain('任意入力');
691
914
  viewModel.setDisplayState({
692
915
  workers: {},
693
916
  conductorOutput: null,
@@ -697,7 +920,7 @@ describe('IssueSessionTui', () => {
697
920
  await flushInkStdin();
698
921
  expect(lastFrame() ?? '').toContain('Open questions');
699
922
  expect(lastFrame() ?? '').toContain('inq-1 (1/1) への回答');
700
- expect(lastFrame() ?? '').not.toContain('任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
923
+ expect(lastFrame() ?? '').not.toContain('任意入力');
701
924
  viewModel.setDisplayState({
702
925
  workers: {},
703
926
  conductorOutput: null,
@@ -706,7 +929,8 @@ describe('IssueSessionTui', () => {
706
929
  });
707
930
  await flushInkStdin();
708
931
  expect(lastFrame() ?? '').not.toContain('Open questions');
709
- expect(lastFrame() ?? '').toContain('任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
932
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
933
+ expect(lastFrame() ?? '').not.toContain('任意入力');
710
934
  });
711
935
  it.each([
712
936
  [
@@ -740,7 +964,7 @@ describe('IssueSessionTui', () => {
740
964
  const initialFrame = lastFrame() ?? '';
741
965
  expect(initialFrame).toContain('Open questions');
742
966
  expect(initialFrame).toContain('inq-resumed-1');
743
- expect(initialFrame).not.toContain('任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
967
+ expect(initialFrame).not.toContain('任意入力');
744
968
  if (selectedIndex === 1) {
745
969
  stdin.write(INK_TEST_KEYS.shiftDownArrow);
746
970
  await flushInkStdin();
@@ -754,16 +978,18 @@ describe('IssueSessionTui', () => {
754
978
  });
755
979
  if (restoredQuestions.length === 1) {
756
980
  expect(lastFrame() ?? '').not.toContain('Open questions');
757
- expect(lastFrame() ?? '').toContain('任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
981
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
982
+ expect(lastFrame() ?? '').not.toContain('任意入力');
758
983
  }
759
984
  else {
760
985
  expect(lastFrame() ?? '').toContain('Open questions');
761
986
  expect(lastFrame() ?? '').toContain('inq-resumed-1');
762
- expect(lastFrame() ?? '').not.toContain('任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
987
+ expect(lastFrame() ?? '').not.toContain('任意入力');
763
988
  setOperatorQuestions([]);
764
989
  await flushInkStdin();
765
990
  expect(lastFrame() ?? '').not.toContain('Open questions');
766
- expect(lastFrame() ?? '').toContain('任意入力 · /reconnect · /close 保存して閉じる · /exit 掃除して終了');
991
+ expect(lastFrame() ?? '').toContain(OPERATOR_INPUT_PROMPT);
992
+ expect(lastFrame() ?? '').not.toContain('任意入力');
767
993
  }
768
994
  });
769
995
  it('renders all activity log label kinds with distinct markers', () => {