@agents-ensemble/cli 0.14.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 (69) hide show
  1. package/README.md +8 -2
  2. package/config.example.yaml +3 -3
  3. package/dist/index.js +5 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/session-log-lines.js +1 -1
  6. package/dist/session-log-lines.js.map +1 -1
  7. package/dist/tui/activity-log.d.ts +1 -1
  8. package/dist/tui/activity-log.d.ts.map +1 -1
  9. package/dist/tui/activity-log.js +16 -2
  10. package/dist/tui/activity-log.js.map +1 -1
  11. package/dist/tui/create-issue-session-tui-host.d.ts.map +1 -1
  12. package/dist/tui/create-issue-session-tui-host.js +1 -1
  13. package/dist/tui/create-issue-session-tui-host.js.map +1 -1
  14. package/dist/tui/ink-test-keys.d.ts +4 -0
  15. package/dist/tui/ink-test-keys.d.ts.map +1 -1
  16. package/dist/tui/ink-test-keys.js +4 -0
  17. package/dist/tui/ink-test-keys.js.map +1 -1
  18. package/dist/tui/issue-session-tui-stream.d.ts.map +1 -1
  19. package/dist/tui/issue-session-tui-stream.js +77 -8
  20. package/dist/tui/issue-session-tui-stream.js.map +1 -1
  21. package/dist/tui/issue-session-tui-stream.test.js +257 -3
  22. package/dist/tui/issue-session-tui-stream.test.js.map +1 -1
  23. package/dist/tui/issue-session-tui.d.ts +3 -2
  24. package/dist/tui/issue-session-tui.d.ts.map +1 -1
  25. package/dist/tui/issue-session-tui.js +145 -12
  26. package/dist/tui/issue-session-tui.js.map +1 -1
  27. package/dist/tui/issue-session-tui.test.js +390 -16
  28. package/dist/tui/issue-session-tui.test.js.map +1 -1
  29. package/dist/tui/open-questions-pane.d.ts +19 -0
  30. package/dist/tui/open-questions-pane.d.ts.map +1 -1
  31. package/dist/tui/open-questions-pane.js +35 -34
  32. package/dist/tui/open-questions-pane.js.map +1 -1
  33. package/dist/tui/operator-command-suggestions.d.ts +15 -0
  34. package/dist/tui/operator-command-suggestions.d.ts.map +1 -1
  35. package/dist/tui/operator-input-history.d.ts +13 -0
  36. package/dist/tui/operator-input-history.d.ts.map +1 -0
  37. package/dist/tui/operator-input-history.js +51 -0
  38. package/dist/tui/operator-input-history.js.map +1 -0
  39. package/dist/tui/operator-input-layout.d.ts +2 -0
  40. package/dist/tui/operator-input-layout.d.ts.map +1 -1
  41. package/dist/tui/operator-input-layout.js +73 -0
  42. package/dist/tui/operator-input-layout.js.map +1 -1
  43. package/dist/tui/operator-text-area.d.ts +2 -0
  44. package/dist/tui/operator-text-area.d.ts.map +1 -1
  45. package/dist/tui/operator-text-area.js +16 -4
  46. package/dist/tui/operator-text-area.js.map +1 -1
  47. package/dist/tui/operator-text-area.test-double.d.ts.map +1 -1
  48. package/dist/tui/operator-text-area.test-double.js +13 -12
  49. package/dist/tui/operator-text-area.test-double.js.map +1 -1
  50. package/dist/tui/operator-text-area.test.js +16 -2
  51. package/dist/tui/operator-text-area.test.js.map +1 -1
  52. package/dist/tui/pane-layout.d.ts +2 -0
  53. package/dist/tui/pane-layout.d.ts.map +1 -1
  54. package/dist/tui/pane-layout.js +7 -3
  55. package/dist/tui/pane-layout.js.map +1 -1
  56. package/dist/tui/stream-layout.d.ts +3 -3
  57. package/dist/tui/stream-layout.d.ts.map +1 -1
  58. package/dist/tui/stream-layout.js +6 -6
  59. package/dist/tui/stream-layout.js.map +1 -1
  60. package/dist/tui/tui-layout-constants.d.ts +1 -1
  61. package/dist/tui/tui-layout-constants.d.ts.map +1 -1
  62. package/dist/tui/tui-layout-constants.js +1 -1
  63. package/dist/tui/tui-layout-constants.js.map +1 -1
  64. package/dist/tui/tui-view-model.d.ts.map +1 -1
  65. package/dist/tui/tui-view-model.js +2 -8
  66. package/dist/tui/tui-view-model.js.map +1 -1
  67. package/docs/config.md +92 -16
  68. package/docs/settings.md +3 -3
  69. 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_PROMPT, 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;
@@ -53,6 +55,7 @@ function fillScrollableHarnessLog(viewModel, count = 30) {
53
55
  const SCROLL_HINT = 'PgUp/PgDn でスクロール';
54
56
  const ISSUE_URL = 'https://github.com/otolab/agents-ensemble/issues/249';
55
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');
56
59
  function createOpenQuestion(overrides) {
57
60
  return {
58
61
  responseType: 'text',
@@ -111,6 +114,153 @@ describe('IssueSessionTui', () => {
111
114
  await flushInkStdin();
112
115
  expect(onSubmit).toHaveBeenCalledWith('/ex', undefined);
113
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
+ });
114
264
  it('sizes the input pane using the shortened prompt width', async () => {
115
265
  Object.defineProperty(process.stdout, 'columns', {
116
266
  configurable: true,
@@ -141,12 +291,12 @@ describe('IssueSessionTui', () => {
141
291
  expect(lastFrame() ?? '').toContain('› /close');
142
292
  stdin.write(INK_TEST_KEYS.downArrow);
143
293
  await flushInkStdin();
144
- expect(lastFrame() ?? '').toContain('› /close');
294
+ expect(lastFrame() ?? '').toContain('› /idle');
145
295
  for (const key of [INK_TEST_KEYS.upArrow, INK_TEST_KEYS.upArrow]) {
146
296
  stdin.write(key);
147
297
  await flushInkStdin();
148
298
  }
149
- expect(lastFrame() ?? '').toContain('› /reconnect');
299
+ expect(lastFrame() ?? '').toContain('› /exit');
150
300
  stdin.write(INK_TEST_KEYS.upArrow);
151
301
  await flushInkStdin();
152
302
  expect(lastFrame() ?? '').toContain('› /reconnect');
@@ -720,6 +870,124 @@ describe('IssueSessionTui', () => {
720
870
  expect(frame).not.toContain('implementer (implementer)');
721
871
  expect(frame).toContain('impl (implementer): running (editing files)');
722
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
+ });
723
991
  it('does not bleed activity log text onto pane border lines', () => {
724
992
  const terminalRows = 24;
725
993
  const liveFrameRows = terminalRows - 1;
@@ -861,15 +1129,17 @@ describe('IssueSessionTui', () => {
861
1129
  hintLineCount,
862
1130
  inputDisplayLineCount: 1,
863
1131
  });
1132
+ const desiredWorkerPaneHeight = computeWorkerPaneHeight(0);
864
1133
  const paneHeights = resolvePaneHeights({
865
1134
  liveFrameRows: 11,
866
- activityPaneHeight: 11 - WORKER_PANE_HEIGHT - requestedInputPaneHeight,
1135
+ workerPaneHeight: desiredWorkerPaneHeight,
1136
+ activityPaneHeight: 11 - desiredWorkerPaneHeight - requestedInputPaneHeight,
867
1137
  openQuestionsPaneHeight: 0,
868
1138
  inputPaneHeight: requestedInputPaneHeight,
869
1139
  });
870
1140
  expect(paneHeights).toEqual({
871
- workerPaneHeight: 5,
872
- activityPaneHeight: 3,
1141
+ workerPaneHeight: 3,
1142
+ activityPaneHeight: 5,
873
1143
  openQuestionsPaneHeight: 0,
874
1144
  inputPaneHeight: 3,
875
1145
  });
@@ -905,6 +1175,31 @@ describe('IssueSessionTui', () => {
905
1175
  expect(frame).not.toContain('任意入力');
906
1176
  expect(frame).not.toContain('/reconnect');
907
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
+ });
908
1203
  it('switches the lower UI between no-question and question modes', async () => {
909
1204
  const viewModel = createTuiViewModel();
910
1205
  const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
@@ -992,16 +1287,28 @@ describe('IssueSessionTui', () => {
992
1287
  expect(lastFrame() ?? '').not.toContain('任意入力');
993
1288
  }
994
1289
  });
995
- it('renders all activity log label kinds with distinct markers', () => {
1290
+ it('separates changing activity labels and keeps consecutive harness entries together', () => {
996
1291
  const viewModel = createTuiViewModel();
997
- viewModel.appendActivityLog('operator', 'op');
998
- viewModel.appendActivityLog('harness', 'tel');
1292
+ viewModel.appendActivityLog('harness', 'tel-1');
1293
+ viewModel.appendActivityLog('harness', 'tel-2');
999
1294
  viewModel.appendActivityLog('observation', 'obs');
1295
+ viewModel.appendActivityLog('operator', 'op');
1000
1296
  viewModel.appendActivityLog('conductor', 'cond');
1001
1297
  const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
1002
1298
  const frame = lastFrame() ?? '';
1003
- expect(frame).toContain('[operator] op');
1004
- 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');
1005
1312
  expect(frame).toContain('[observation] obs');
1006
1313
  expect(frame).toContain('[conductor] cond');
1007
1314
  });
@@ -1196,6 +1503,73 @@ describe('IssueSessionTui', () => {
1196
1503
  expect(secondIndex).toBeGreaterThan(firstIndex + 1);
1197
1504
  });
1198
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
+ });
1199
1573
  it('cycles selection with Shift+arrow keys', async () => {
1200
1574
  const viewModel = createTuiViewModel();
1201
1575
  viewModel.setDisplayState({
@@ -1339,13 +1713,13 @@ describe('IssueSessionTui', () => {
1339
1713
  const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
1340
1714
  const pinnedFrame = lastFrame() ?? '';
1341
1715
  expect(pinnedFrame).toContain('[harness] line-29');
1342
- expect(pinnedFrame).not.toContain('[harness] line-16');
1716
+ expect(pinnedFrame).not.toContain('[harness] line-14');
1343
1717
  expect(pinnedFrame).not.toContain(SCROLL_HINT);
1344
1718
  stdin.write(INK_TEST_KEYS.pageUp);
1345
1719
  await flushInkStdin();
1346
1720
  const scrolledFrame = lastFrame() ?? '';
1347
1721
  expect(scrolledFrame).toContain(SCROLL_HINT);
1348
- expect(scrolledFrame).toContain('[harness] line-16');
1722
+ expect(scrolledFrame).toContain('[harness] line-14');
1349
1723
  expect(scrolledFrame).not.toContain('[harness] line-29');
1350
1724
  });
1351
1725
  it('returns to latest lines on End and clears scroll hint', async () => {
@@ -1367,11 +1741,11 @@ describe('IssueSessionTui', () => {
1367
1741
  const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
1368
1742
  stdin.write(INK_TEST_KEYS.pageUp);
1369
1743
  await flushInkStdin();
1370
- expect(lastFrame() ?? '').toContain('[harness] line-16');
1744
+ expect(lastFrame() ?? '').toContain('[harness] line-14');
1371
1745
  viewModel.appendActivityLog('harness', 'newest-marker');
1372
1746
  await flushInkStdin();
1373
1747
  const detachedFrame = lastFrame() ?? '';
1374
- expect(detachedFrame).toContain('[harness] line-16');
1748
+ expect(detachedFrame).toContain('[harness] line-14');
1375
1749
  expect(detachedFrame).not.toContain('[harness] newest-marker');
1376
1750
  stdin.write(INK_TEST_KEYS.end);
1377
1751
  await flushInkStdin();
@@ -1400,7 +1774,7 @@ describe('IssueSessionTui', () => {
1400
1774
  const ctrlPageUpFrame = lastFrame() ?? '';
1401
1775
  expect(ctrlPageUpFrame).toContain('typed');
1402
1776
  expect(ctrlPageUpFrame).toContain(SCROLL_HINT);
1403
- expect(ctrlPageUpFrame).toContain('[harness] line-16');
1777
+ expect(ctrlPageUpFrame).toContain('[harness] line-14');
1404
1778
  expect(ctrlPageUpFrame).not.toContain('[harness] line-29');
1405
1779
  });
1406
1780
  });