@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.
- package/README.md +8 -2
- package/config.example.yaml +3 -3
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/session-log-lines.js +1 -1
- package/dist/session-log-lines.js.map +1 -1
- package/dist/tui/activity-log.d.ts +1 -1
- package/dist/tui/activity-log.d.ts.map +1 -1
- package/dist/tui/activity-log.js +16 -2
- package/dist/tui/activity-log.js.map +1 -1
- package/dist/tui/create-issue-session-tui-host.d.ts.map +1 -1
- package/dist/tui/create-issue-session-tui-host.js +1 -1
- package/dist/tui/create-issue-session-tui-host.js.map +1 -1
- package/dist/tui/ink-test-keys.d.ts +4 -0
- package/dist/tui/ink-test-keys.d.ts.map +1 -1
- package/dist/tui/ink-test-keys.js +4 -0
- package/dist/tui/ink-test-keys.js.map +1 -1
- package/dist/tui/issue-session-tui-stream.d.ts.map +1 -1
- package/dist/tui/issue-session-tui-stream.js +77 -8
- package/dist/tui/issue-session-tui-stream.js.map +1 -1
- package/dist/tui/issue-session-tui-stream.test.js +257 -3
- package/dist/tui/issue-session-tui-stream.test.js.map +1 -1
- package/dist/tui/issue-session-tui.d.ts +3 -2
- package/dist/tui/issue-session-tui.d.ts.map +1 -1
- package/dist/tui/issue-session-tui.js +145 -12
- package/dist/tui/issue-session-tui.js.map +1 -1
- package/dist/tui/issue-session-tui.test.js +390 -16
- package/dist/tui/issue-session-tui.test.js.map +1 -1
- package/dist/tui/open-questions-pane.d.ts +19 -0
- package/dist/tui/open-questions-pane.d.ts.map +1 -1
- package/dist/tui/open-questions-pane.js +35 -34
- package/dist/tui/open-questions-pane.js.map +1 -1
- package/dist/tui/operator-command-suggestions.d.ts +15 -0
- package/dist/tui/operator-command-suggestions.d.ts.map +1 -1
- package/dist/tui/operator-input-history.d.ts +13 -0
- package/dist/tui/operator-input-history.d.ts.map +1 -0
- package/dist/tui/operator-input-history.js +51 -0
- package/dist/tui/operator-input-history.js.map +1 -0
- package/dist/tui/operator-input-layout.d.ts +2 -0
- package/dist/tui/operator-input-layout.d.ts.map +1 -1
- package/dist/tui/operator-input-layout.js +73 -0
- package/dist/tui/operator-input-layout.js.map +1 -1
- package/dist/tui/operator-text-area.d.ts +2 -0
- package/dist/tui/operator-text-area.d.ts.map +1 -1
- package/dist/tui/operator-text-area.js +16 -4
- package/dist/tui/operator-text-area.js.map +1 -1
- package/dist/tui/operator-text-area.test-double.d.ts.map +1 -1
- package/dist/tui/operator-text-area.test-double.js +13 -12
- package/dist/tui/operator-text-area.test-double.js.map +1 -1
- package/dist/tui/operator-text-area.test.js +16 -2
- package/dist/tui/operator-text-area.test.js.map +1 -1
- package/dist/tui/pane-layout.d.ts +2 -0
- package/dist/tui/pane-layout.d.ts.map +1 -1
- package/dist/tui/pane-layout.js +7 -3
- package/dist/tui/pane-layout.js.map +1 -1
- package/dist/tui/stream-layout.d.ts +3 -3
- package/dist/tui/stream-layout.d.ts.map +1 -1
- package/dist/tui/stream-layout.js +6 -6
- package/dist/tui/stream-layout.js.map +1 -1
- package/dist/tui/tui-layout-constants.d.ts +1 -1
- package/dist/tui/tui-layout-constants.d.ts.map +1 -1
- package/dist/tui/tui-layout-constants.js +1 -1
- package/dist/tui/tui-layout-constants.js.map +1 -1
- package/dist/tui/tui-view-model.d.ts.map +1 -1
- package/dist/tui/tui-view-model.js +2 -8
- package/dist/tui/tui-view-model.js.map +1 -1
- package/docs/config.md +92 -16
- package/docs/settings.md +3 -3
- package/package.json +3 -3
|
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
vi.mock('./operator-text-area.js', () => import('./operator-text-area.test-double.js'));
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import { cleanup, render } from 'ink-testing-library';
|
|
6
|
+
import stringWidth from 'string-width';
|
|
6
7
|
import { IssueSessionTui } from './issue-session-tui.js';
|
|
7
8
|
import { createTuiViewModel } from './tui-view-model.js';
|
|
8
9
|
import { buildActivityLogDisplayLines } from './activity-log.js';
|
|
@@ -10,10 +11,11 @@ import { resolveOperatorInputDisplayMode } from './format-operator-context.js';
|
|
|
10
11
|
import { flushInkStdin, INK_TEST_KEYS } from './ink-test-keys.js';
|
|
11
12
|
import { extractOrchestrationPaneFrameStats, extractTuiPaneFrameStats } from './orchestration-pane-frame.js';
|
|
12
13
|
import { resolveOpenQuestionsPaneLayout } from './open-questions-pane.js';
|
|
14
|
+
import { computeWorkerPaneHeight } from './pane-layout.js';
|
|
13
15
|
import { computeActivityLogLineCapacity, computeActivityPaneHeight, computeInputPaneHeight, computeOrchestrationLogVisibleLineCount, computeOperatorInputCursorX, computeOperatorInputLineIndex, } from './compute-operator-input-cursor-y.js';
|
|
14
16
|
import { getPaneContentWidth } from './wrap-text-to-width.js';
|
|
15
17
|
import { resolvePaneHeights } from './pane-layout.js';
|
|
16
|
-
import { INPUT_PANE_TITLE, MAIN_PANE_TITLE, OPEN_QUESTIONS_PANE_MIN_HEIGHT, OPERATOR_INPUT_PROMPT, OPERATOR_INPUT_CURSOR_Y_OFFSET, PANE_PADDING_X, ROUND_BORDER_WIDTH,
|
|
18
|
+
import { INPUT_PANE_TITLE, MAIN_PANE_TITLE, OPEN_QUESTIONS_PANE_MIN_HEIGHT, OPERATOR_INPUT_PROMPT, OPERATOR_INPUT_CURSOR_Y_OFFSET, PANE_PADDING_X, ROUND_BORDER_WIDTH, WORKER_PANE_TITLE, } from './tui-layout-constants.js';
|
|
17
19
|
import { createTuiResizeController, createTuiTerminalSizeStore, TUI_RESIZE_SETTLE_MS, TUI_RESIZE_SHRINK_COALESCE_MS, } from './tui-terminal-size.js';
|
|
18
20
|
class ResizeSource extends EventEmitter {
|
|
19
21
|
columns = 80;
|
|
@@ -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('› /
|
|
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('› /
|
|
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
|
-
|
|
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:
|
|
872
|
-
activityPaneHeight:
|
|
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('
|
|
1290
|
+
it('separates changing activity labels and keeps consecutive harness entries together', () => {
|
|
996
1291
|
const viewModel = createTuiViewModel();
|
|
997
|
-
viewModel.appendActivityLog('
|
|
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
|
-
|
|
1004
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1777
|
+
expect(ctrlPageUpFrame).toContain('[harness] line-14');
|
|
1404
1778
|
expect(ctrlPageUpFrame).not.toContain('[harness] line-29');
|
|
1405
1779
|
});
|
|
1406
1780
|
});
|