@agents-ensemble/cli 0.6.4 → 0.7.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.
- package/README.md +36 -3
- package/config.example.yaml +13 -0
- package/dist/async-operator-input.js +1 -1
- package/dist/async-operator-input.js.map +1 -1
- package/dist/display/session-display-reducer.d.ts.map +1 -1
- package/dist/display/session-display-reducer.js +0 -11
- package/dist/display/session-display-reducer.js.map +1 -1
- package/dist/inline-markdown.d.ts +3 -1
- package/dist/inline-markdown.d.ts.map +1 -1
- package/dist/inline-markdown.js +232 -33
- package/dist/inline-markdown.js.map +1 -1
- package/dist/issue-command.d.ts.map +1 -1
- package/dist/issue-command.js +8 -4
- package/dist/issue-command.js.map +1 -1
- package/dist/prompt-operator-input.d.ts +1 -1
- package/dist/prompt-operator-input.d.ts.map +1 -1
- package/dist/prompt-operator-input.js +1 -1
- package/dist/prompt-operator-input.js.map +1 -1
- package/dist/session-log-lines.d.ts.map +1 -1
- package/dist/session-log-lines.js +64 -8
- package/dist/session-log-lines.js.map +1 -1
- package/dist/session-sinks.d.ts.map +1 -1
- package/dist/session-sinks.js +2 -10
- package/dist/session-sinks.js.map +1 -1
- package/dist/tui/activity-log.d.ts +8 -0
- package/dist/tui/activity-log.d.ts.map +1 -1
- package/dist/tui/activity-log.js +16 -0
- package/dist/tui/activity-log.js.map +1 -1
- package/dist/tui/compute-operator-input-cursor-y.d.ts +4 -0
- package/dist/tui/compute-operator-input-cursor-y.d.ts.map +1 -1
- package/dist/tui/compute-operator-input-cursor-y.js +4 -2
- package/dist/tui/compute-operator-input-cursor-y.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 +44 -8
- package/dist/tui/create-issue-session-tui-host.js.map +1 -1
- package/dist/tui/create-issue-session-tui-host.test.js +19 -5
- package/dist/tui/create-issue-session-tui-host.test.js.map +1 -1
- package/dist/tui/issue-session-tui-stream.d.ts +6 -1
- package/dist/tui/issue-session-tui-stream.d.ts.map +1 -1
- package/dist/tui/issue-session-tui-stream.js +55 -17
- package/dist/tui/issue-session-tui-stream.js.map +1 -1
- package/dist/tui/issue-session-tui-stream.test.js +345 -11
- package/dist/tui/issue-session-tui-stream.test.js.map +1 -1
- package/dist/tui/issue-session-tui.d.ts +11 -5
- package/dist/tui/issue-session-tui.d.ts.map +1 -1
- package/dist/tui/issue-session-tui.js +60 -30
- package/dist/tui/issue-session-tui.js.map +1 -1
- package/dist/tui/issue-session-tui.test.js +268 -30
- package/dist/tui/issue-session-tui.test.js.map +1 -1
- package/dist/tui/open-questions-pane.d.ts.map +1 -1
- package/dist/tui/open-questions-pane.js +30 -12
- package/dist/tui/open-questions-pane.js.map +1 -1
- package/dist/tui/pane-layout.d.ts +22 -0
- package/dist/tui/pane-layout.d.ts.map +1 -0
- package/dist/tui/pane-layout.js +74 -0
- package/dist/tui/pane-layout.js.map +1 -0
- package/dist/tui/stream-scrollback.d.ts +29 -0
- package/dist/tui/stream-scrollback.d.ts.map +1 -0
- package/dist/tui/stream-scrollback.js +35 -0
- package/dist/tui/stream-scrollback.js.map +1 -0
- package/dist/tui/titled-border-pane.d.ts +2 -1
- package/dist/tui/titled-border-pane.d.ts.map +1 -1
- package/dist/tui/titled-border-pane.js +2 -2
- package/dist/tui/titled-border-pane.js.map +1 -1
- package/dist/tui/tui-layout-constants.d.ts +11 -6
- package/dist/tui/tui-layout-constants.d.ts.map +1 -1
- package/dist/tui/tui-layout-constants.js +11 -6
- package/dist/tui/tui-layout-constants.js.map +1 -1
- package/dist/tui/tui-resize-recovery.d.ts +24 -0
- package/dist/tui/tui-resize-recovery.d.ts.map +1 -0
- package/dist/tui/tui-resize-recovery.js +34 -0
- package/dist/tui/tui-resize-recovery.js.map +1 -0
- package/dist/tui/tui-terminal-size.d.ts +45 -0
- package/dist/tui/tui-terminal-size.d.ts.map +1 -0
- package/dist/tui/tui-terminal-size.js +195 -0
- package/dist/tui/tui-terminal-size.js.map +1 -0
- package/dist/tui/tui-view-model.d.ts +2 -0
- package/dist/tui/tui-view-model.d.ts.map +1 -1
- package/dist/tui/tui-view-model.js +7 -0
- package/dist/tui/tui-view-model.js.map +1 -1
- package/dist/tui/wrap-text-to-width.d.ts.map +1 -1
- package/dist/tui/wrap-text-to-width.js +4 -1
- package/dist/tui/wrap-text-to-width.js.map +1 -1
- package/docs/config.md +67 -3
- package/docs/settings.md +17 -4
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
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
|
+
import { EventEmitter } from 'node:events';
|
|
4
5
|
import { cleanup, render } from 'ink-testing-library';
|
|
5
6
|
import { IssueSessionTui } from './issue-session-tui.js';
|
|
6
7
|
import { createTuiViewModel } from './tui-view-model.js';
|
|
@@ -9,9 +10,15 @@ import { formatOperatorContextHint } from './format-operator-context.js';
|
|
|
9
10
|
import { flushInkStdin, INK_TEST_KEYS } from './ink-test-keys.js';
|
|
10
11
|
import { extractOrchestrationPaneFrameStats, extractTuiPaneFrameStats } from './orchestration-pane-frame.js';
|
|
11
12
|
import { resolveOpenQuestionsPaneLayout } from './open-questions-pane.js';
|
|
12
|
-
import { computeActivityLogLineCapacity, computeActivityPaneHeight, computeOrchestrationLogVisibleLineCount, computeOperatorInputCursorX, computeOperatorInputLineIndex, } from './compute-operator-input-cursor-y.js';
|
|
13
|
+
import { computeActivityLogLineCapacity, computeActivityPaneHeight, computeInputPaneHeight, computeOrchestrationLogVisibleLineCount, computeOperatorInputCursorX, computeOperatorInputLineIndex, } from './compute-operator-input-cursor-y.js';
|
|
13
14
|
import { getPaneContentWidth, wrapTextToWidth } from './wrap-text-to-width.js';
|
|
14
|
-
import {
|
|
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';
|
|
17
|
+
import { createTuiResizeController, createTuiTerminalSizeStore, TUI_RESIZE_SETTLE_MS, TUI_RESIZE_SHRINK_COALESCE_MS, } from './tui-terminal-size.js';
|
|
18
|
+
class ResizeSource extends EventEmitter {
|
|
19
|
+
columns = 80;
|
|
20
|
+
rows = 24;
|
|
21
|
+
}
|
|
15
22
|
function findOperatorInputLine(lines) {
|
|
16
23
|
const operatorLineIndices = lines
|
|
17
24
|
.map((line, index) => (line.includes('operator>') ? index : -1))
|
|
@@ -30,6 +37,13 @@ function expectNoContentOnBorderLines(frame) {
|
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
}
|
|
40
|
+
function expectCompleteTitledPaneFrame(frame, title) {
|
|
41
|
+
const lines = frame.split('\n');
|
|
42
|
+
const topBorderIndex = lines.findIndex((line) => (line.startsWith('╭') || line.startsWith('┌')) && line.includes(title));
|
|
43
|
+
expect(topBorderIndex).toBeGreaterThanOrEqual(0);
|
|
44
|
+
const bottomBorderIndex = lines.findIndex((line, index) => index > topBorderIndex && (line.startsWith('╰') || line.startsWith('└')));
|
|
45
|
+
expect(bottomBorderIndex).toBeGreaterThan(topBorderIndex);
|
|
46
|
+
}
|
|
33
47
|
function fillScrollableHarnessLog(viewModel, count = 30) {
|
|
34
48
|
for (let index = 0; index < count; index++) {
|
|
35
49
|
viewModel.appendActivityLog('harness', `line-${index}`);
|
|
@@ -72,6 +86,7 @@ describe('IssueSessionTui', () => {
|
|
|
72
86
|
});
|
|
73
87
|
afterEach(() => {
|
|
74
88
|
cleanup();
|
|
89
|
+
vi.useRealTimers();
|
|
75
90
|
vi.restoreAllMocks();
|
|
76
91
|
});
|
|
77
92
|
it('renders four panes with worker, session log, open questions, and input', () => {
|
|
@@ -136,7 +151,7 @@ describe('IssueSessionTui', () => {
|
|
|
136
151
|
viewModel.setPostLoopWaiting(true);
|
|
137
152
|
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
138
153
|
const frame = lastFrame() ?? '';
|
|
139
|
-
expect(frame).toContain('追加指示を入力するか /exit で終了');
|
|
154
|
+
expect(frame).toContain('追加指示を入力するか /reconnect で再接続 · /exit で終了');
|
|
140
155
|
expect(frame).not.toContain('post-loop 待機中');
|
|
141
156
|
});
|
|
142
157
|
it('shows the issue link in the workers pane header and prompt-only operator input', async () => {
|
|
@@ -155,8 +170,8 @@ describe('IssueSessionTui', () => {
|
|
|
155
170
|
expect(frame).toContain(issueLabel);
|
|
156
171
|
expect(frame).toContain(osc8Open);
|
|
157
172
|
expect(frame).not.toContain(nonCanonicalOsc8Open);
|
|
158
|
-
expect(frame).toContain('任意のタイミングで入力 · /exit で終了');
|
|
159
|
-
expect(frame).not.toContain(`${issueLabel} — 任意のタイミングで入力 · /exit で終了`);
|
|
173
|
+
expect(frame).toContain('任意のタイミングで入力 · /reconnect で再接続 · /exit で終了');
|
|
174
|
+
expect(frame).not.toContain(`${issueLabel} — 任意のタイミングで入力 · /reconnect で再接続 · /exit で終了`);
|
|
160
175
|
viewModel.setDisplayState({
|
|
161
176
|
workers: {},
|
|
162
177
|
conductorOutput: null,
|
|
@@ -191,7 +206,7 @@ describe('IssueSessionTui', () => {
|
|
|
191
206
|
expect(lastFrame() ?? '').toContain(issueLabel);
|
|
192
207
|
expect(lastFrame() ?? '').toContain(osc8Open);
|
|
193
208
|
expect(lastFrame() ?? '').not.toContain(nonCanonicalOsc8Open);
|
|
194
|
-
expect(lastFrame() ?? '').toContain('追加指示を入力するか /exit で終了');
|
|
209
|
+
expect(lastFrame() ?? '').toContain('追加指示を入力するか /reconnect で再接続 · /exit で終了');
|
|
195
210
|
expect(lastFrame() ?? '').not.toContain('post-loop 待機中');
|
|
196
211
|
viewModel.setShuttingDown(true);
|
|
197
212
|
await flushInkStdin();
|
|
@@ -345,6 +360,7 @@ describe('IssueSessionTui', () => {
|
|
|
345
360
|
});
|
|
346
361
|
it('aligns IME cursor coordinates with the rendered operator input line', () => {
|
|
347
362
|
const terminalRows = 24;
|
|
363
|
+
const liveFrameRows = terminalRows - 1;
|
|
348
364
|
const terminalColumns = 80;
|
|
349
365
|
Object.defineProperty(process.stdout, 'columns', {
|
|
350
366
|
configurable: true,
|
|
@@ -371,7 +387,7 @@ describe('IssueSessionTui', () => {
|
|
|
371
387
|
const hintLineCount = wrapTextToWidth(contextHint, contentWidth).length;
|
|
372
388
|
const openQuestionsPaneHeight = 0;
|
|
373
389
|
const expectedInputLineIndex = computeOperatorInputLineIndex({
|
|
374
|
-
terminalRows,
|
|
390
|
+
terminalRows: liveFrameRows,
|
|
375
391
|
hintLineCount,
|
|
376
392
|
openQuestionsPaneHeight,
|
|
377
393
|
});
|
|
@@ -384,10 +400,11 @@ describe('IssueSessionTui', () => {
|
|
|
384
400
|
expect(inputStartX).toBe(expectedCursorX);
|
|
385
401
|
expect(lineIndex).toBe(expectedInputLineIndex);
|
|
386
402
|
expect(expectedCursorY).toBe(lineIndex + OPERATOR_INPUT_CURSOR_Y_OFFSET);
|
|
387
|
-
expect(lines).toHaveLength(
|
|
403
|
+
expect(lines).toHaveLength(liveFrameRows);
|
|
388
404
|
});
|
|
389
405
|
it('aligns IME cursor coordinates when context hint wraps on a narrow terminal', () => {
|
|
390
406
|
const terminalRows = 24;
|
|
407
|
+
const liveFrameRows = terminalRows - 1;
|
|
391
408
|
const terminalColumns = 40;
|
|
392
409
|
Object.defineProperty(process.stdout, 'columns', {
|
|
393
410
|
configurable: true,
|
|
@@ -414,7 +431,7 @@ describe('IssueSessionTui', () => {
|
|
|
414
431
|
const hintLineCount = wrapTextToWidth(contextHint, contentWidth).length;
|
|
415
432
|
const openQuestionsPaneHeight = 0;
|
|
416
433
|
const expectedInputLineIndex = computeOperatorInputLineIndex({
|
|
417
|
-
terminalRows,
|
|
434
|
+
terminalRows: liveFrameRows,
|
|
418
435
|
hintLineCount,
|
|
419
436
|
openQuestionsPaneHeight,
|
|
420
437
|
});
|
|
@@ -428,7 +445,7 @@ describe('IssueSessionTui', () => {
|
|
|
428
445
|
expect(inputStartX).toBe(expectedCursorX);
|
|
429
446
|
expect(lineIndex).toBe(expectedInputLineIndex);
|
|
430
447
|
expect(expectedCursorY).toBe(lineIndex + OPERATOR_INPUT_CURSOR_Y_OFFSET);
|
|
431
|
-
expect(lines).toHaveLength(
|
|
448
|
+
expect(lines).toHaveLength(liveFrameRows);
|
|
432
449
|
});
|
|
433
450
|
it('shows conductor thinking during in-flight send then idle on completion', () => {
|
|
434
451
|
const viewModel = createTuiViewModel();
|
|
@@ -459,6 +476,7 @@ describe('IssueSessionTui', () => {
|
|
|
459
476
|
});
|
|
460
477
|
it('does not bleed activity log text onto pane border lines', () => {
|
|
461
478
|
const terminalRows = 24;
|
|
479
|
+
const liveFrameRows = terminalRows - 1;
|
|
462
480
|
const terminalColumns = 80;
|
|
463
481
|
Object.defineProperty(process.stdout, 'columns', {
|
|
464
482
|
configurable: true,
|
|
@@ -501,7 +519,137 @@ describe('IssueSessionTui', () => {
|
|
|
501
519
|
const frame = lastFrame() ?? '';
|
|
502
520
|
expect(frame).toContain(WORKER_PANE_TITLE);
|
|
503
521
|
expectNoContentOnBorderLines(frame);
|
|
504
|
-
expect(frame.split('\n')).toHaveLength(
|
|
522
|
+
expect(frame.split('\n')).toHaveLength(liveFrameRows);
|
|
523
|
+
});
|
|
524
|
+
it('recomputes pane dimensions from the settled terminal size snapshot', async () => {
|
|
525
|
+
vi.useFakeTimers();
|
|
526
|
+
const source = new ResizeSource();
|
|
527
|
+
const terminalSizeStore = createTuiTerminalSizeStore(source);
|
|
528
|
+
const viewModel = createTuiViewModel();
|
|
529
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, terminalSizeStore: terminalSizeStore, onSubmit: () => { } }));
|
|
530
|
+
expect((lastFrame() ?? '').split('\n')).toHaveLength(23);
|
|
531
|
+
source.columns = 40;
|
|
532
|
+
source.rows = 12;
|
|
533
|
+
source.emit('resize');
|
|
534
|
+
await vi.advanceTimersByTimeAsync(TUI_RESIZE_SHRINK_COALESCE_MS);
|
|
535
|
+
const frame = lastFrame() ?? '';
|
|
536
|
+
expect(frame.split('\n')).toHaveLength(11);
|
|
537
|
+
expect(Math.max(...frame.split('\n').map((line) => line.trimEnd().length))).toBeLessThanOrEqual(40);
|
|
538
|
+
expect(frame).toContain('operator>');
|
|
539
|
+
terminalSizeStore.dispose();
|
|
540
|
+
});
|
|
541
|
+
it('coalesces staged same-row shrink before redrawing the pane frame', async () => {
|
|
542
|
+
vi.useFakeTimers();
|
|
543
|
+
const source = new ResizeSource();
|
|
544
|
+
source.columns = 120;
|
|
545
|
+
source.rows = 32;
|
|
546
|
+
const resizeController = createTuiResizeController(source);
|
|
547
|
+
const terminalSizeStore = resizeController.terminalSize;
|
|
548
|
+
const viewModel = createTuiViewModel();
|
|
549
|
+
viewModel.setPostLoopWaiting(true);
|
|
550
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, terminalSizeStore: terminalSizeStore, onSubmit: () => { } }));
|
|
551
|
+
const initialFrame = lastFrame() ?? '';
|
|
552
|
+
const inkResizeNotifications = [];
|
|
553
|
+
const inkResizeListener = () => {
|
|
554
|
+
inkResizeNotifications.push({
|
|
555
|
+
terminalColumns: terminalSizeStore.getSnapshot().columns,
|
|
556
|
+
proxyColumns: resizeController.stdout.columns,
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
resizeController.stdout.on('resize', inkResizeListener);
|
|
560
|
+
const unsubscribe = terminalSizeStore.subscribe(() => {
|
|
561
|
+
expect(resizeController.stdout.columns).toBeGreaterThanOrEqual(terminalSizeStore.getSnapshot().columns);
|
|
562
|
+
});
|
|
563
|
+
try {
|
|
564
|
+
for (const columns of [110, 100, 90, 80, 70]) {
|
|
565
|
+
source.columns = columns;
|
|
566
|
+
source.rows = 32;
|
|
567
|
+
source.emit('resize');
|
|
568
|
+
await vi.advanceTimersByTimeAsync(TUI_RESIZE_SETTLE_MS);
|
|
569
|
+
expect(inkResizeNotifications).toEqual([]);
|
|
570
|
+
expect(lastFrame()).toBe(initialFrame);
|
|
571
|
+
}
|
|
572
|
+
source.columns = 60;
|
|
573
|
+
source.rows = 32;
|
|
574
|
+
source.emit('resize');
|
|
575
|
+
await vi.advanceTimersByTimeAsync(TUI_RESIZE_SHRINK_COALESCE_MS);
|
|
576
|
+
const finalFrame = lastFrame() ?? '';
|
|
577
|
+
expect(inkResizeNotifications).toEqual([
|
|
578
|
+
{ terminalColumns: 60, proxyColumns: 120 },
|
|
579
|
+
]);
|
|
580
|
+
expect(finalFrame).not.toBe(initialFrame);
|
|
581
|
+
expect(Math.max(...finalFrame.split('\n').map((line) => line.trimEnd().length))).toBeLessThanOrEqual(60);
|
|
582
|
+
expect(finalFrame).toContain(WORKER_PANE_TITLE);
|
|
583
|
+
expect(finalFrame).toContain('operator>');
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
unsubscribe();
|
|
587
|
+
resizeController.dispose();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
it('keeps every pane frame and the operator input row intact at 60x12 after resize', async () => {
|
|
591
|
+
vi.useFakeTimers();
|
|
592
|
+
const source = new ResizeSource();
|
|
593
|
+
source.columns = 120;
|
|
594
|
+
source.rows = 32;
|
|
595
|
+
const terminalSizeStore = createTuiTerminalSizeStore(source);
|
|
596
|
+
const viewModel = createTuiViewModel();
|
|
597
|
+
viewModel.setPostLoopWaiting(true);
|
|
598
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, terminalSizeStore: terminalSizeStore, onSubmit: () => { } }));
|
|
599
|
+
try {
|
|
600
|
+
source.columns = 60;
|
|
601
|
+
source.rows = 12;
|
|
602
|
+
source.emit('resize');
|
|
603
|
+
await vi.advanceTimersByTimeAsync(TUI_RESIZE_SHRINK_COALESCE_MS);
|
|
604
|
+
const frame = lastFrame() ?? '';
|
|
605
|
+
const lines = frame.split('\n');
|
|
606
|
+
expect(lines).toHaveLength(11);
|
|
607
|
+
expect(Math.max(...lines.map((line) => line.trimEnd().length))).toBeLessThanOrEqual(60);
|
|
608
|
+
for (const title of [WORKER_PANE_TITLE, MAIN_PANE_TITLE, INPUT_PANE_TITLE]) {
|
|
609
|
+
expectCompleteTitledPaneFrame(frame, title);
|
|
610
|
+
const stats = extractTuiPaneFrameStats(frame, title);
|
|
611
|
+
expect(stats.titleOnBorder).toBe(true);
|
|
612
|
+
}
|
|
613
|
+
const contentWidth = getPaneContentWidth({
|
|
614
|
+
columns: 60,
|
|
615
|
+
paddingX: PANE_PADDING_X,
|
|
616
|
+
borderWidth: ROUND_BORDER_WIDTH,
|
|
617
|
+
});
|
|
618
|
+
const hintLineCount = wrapTextToWidth(OPERATOR_INPUT_POST_LOOP_HINT, contentWidth).length;
|
|
619
|
+
const requestedInputPaneHeight = computeInputPaneHeight({
|
|
620
|
+
hintLineCount,
|
|
621
|
+
inputDisplayLineCount: 1,
|
|
622
|
+
});
|
|
623
|
+
const paneHeights = resolvePaneHeights({
|
|
624
|
+
liveFrameRows: 11,
|
|
625
|
+
activityPaneHeight: 11 - WORKER_PANE_HEIGHT - requestedInputPaneHeight,
|
|
626
|
+
openQuestionsPaneHeight: 0,
|
|
627
|
+
inputPaneHeight: requestedInputPaneHeight,
|
|
628
|
+
});
|
|
629
|
+
expect(paneHeights).toEqual({
|
|
630
|
+
workerPaneHeight: 4,
|
|
631
|
+
activityPaneHeight: 3,
|
|
632
|
+
openQuestionsPaneHeight: 0,
|
|
633
|
+
inputPaneHeight: 4,
|
|
634
|
+
});
|
|
635
|
+
const inputLine = findOperatorInputLine(lines);
|
|
636
|
+
expect(inputLine.lineIndex).toBe(computeOperatorInputLineIndex({
|
|
637
|
+
terminalRows: 11,
|
|
638
|
+
hintLineCount,
|
|
639
|
+
inputDisplayLineCount: 1,
|
|
640
|
+
openQuestionsPaneHeight: 0,
|
|
641
|
+
workerPaneHeight: paneHeights.workerPaneHeight,
|
|
642
|
+
activityPaneHeight: paneHeights.activityPaneHeight,
|
|
643
|
+
}));
|
|
644
|
+
expect(inputLine.lineIndex).toBeGreaterThanOrEqual(0);
|
|
645
|
+
expect(inputLine.lineIndex).toBeLessThan(lines.length);
|
|
646
|
+
expect(inputLine.inputStartX).toBeGreaterThanOrEqual(0);
|
|
647
|
+
expect(frame).toContain('Orchestration');
|
|
648
|
+
expect(frame).toContain('operator>');
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
terminalSizeStore.dispose();
|
|
652
|
+
}
|
|
505
653
|
});
|
|
506
654
|
it('shows the no-question input mode when no session activity yet', () => {
|
|
507
655
|
const viewModel = createTuiViewModel();
|
|
@@ -511,13 +659,13 @@ describe('IssueSessionTui', () => {
|
|
|
511
659
|
expect(frame).toContain('(活動ログなし)');
|
|
512
660
|
expect(frame).not.toContain('Open questions');
|
|
513
661
|
expect(frame).not.toContain('(未回答なし)');
|
|
514
|
-
expect(frame).toContain('任意のタイミングで入力 · /exit で終了');
|
|
662
|
+
expect(frame).toContain('任意のタイミングで入力 · /reconnect で再接続 · /exit で終了');
|
|
515
663
|
});
|
|
516
664
|
it('switches the lower UI between no-question and question modes', async () => {
|
|
517
665
|
const viewModel = createTuiViewModel();
|
|
518
666
|
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
519
667
|
expect(lastFrame() ?? '').not.toContain('Open questions');
|
|
520
|
-
expect(lastFrame() ?? '').toContain('任意のタイミングで入力 · /exit で終了');
|
|
668
|
+
expect(lastFrame() ?? '').toContain('任意のタイミングで入力 · /reconnect で再接続 · /exit で終了');
|
|
521
669
|
viewModel.setDisplayState({
|
|
522
670
|
workers: {},
|
|
523
671
|
conductorOutput: null,
|
|
@@ -527,7 +675,7 @@ describe('IssueSessionTui', () => {
|
|
|
527
675
|
await flushInkStdin();
|
|
528
676
|
expect(lastFrame() ?? '').toContain('Open questions');
|
|
529
677
|
expect(lastFrame() ?? '').toContain('inq-1 (1/1) への回答');
|
|
530
|
-
expect(lastFrame() ?? '').not.toContain('任意のタイミングで入力 · /exit で終了');
|
|
678
|
+
expect(lastFrame() ?? '').not.toContain('任意のタイミングで入力 · /reconnect で再接続 · /exit で終了');
|
|
531
679
|
viewModel.setDisplayState({
|
|
532
680
|
workers: {},
|
|
533
681
|
conductorOutput: null,
|
|
@@ -536,7 +684,7 @@ describe('IssueSessionTui', () => {
|
|
|
536
684
|
});
|
|
537
685
|
await flushInkStdin();
|
|
538
686
|
expect(lastFrame() ?? '').not.toContain('Open questions');
|
|
539
|
-
expect(lastFrame() ?? '').toContain('任意のタイミングで入力 · /exit で終了');
|
|
687
|
+
expect(lastFrame() ?? '').toContain('任意のタイミングで入力 · /reconnect で再接続 · /exit で終了');
|
|
540
688
|
});
|
|
541
689
|
it.each([
|
|
542
690
|
[
|
|
@@ -570,7 +718,7 @@ describe('IssueSessionTui', () => {
|
|
|
570
718
|
const initialFrame = lastFrame() ?? '';
|
|
571
719
|
expect(initialFrame).toContain('Open questions');
|
|
572
720
|
expect(initialFrame).toContain('inq-resumed-1');
|
|
573
|
-
expect(initialFrame).not.toContain('任意のタイミングで入力 · /exit で終了');
|
|
721
|
+
expect(initialFrame).not.toContain('任意のタイミングで入力 · /reconnect で再接続 · /exit で終了');
|
|
574
722
|
if (selectedIndex === 1) {
|
|
575
723
|
stdin.write(INK_TEST_KEYS.shiftDownArrow);
|
|
576
724
|
await flushInkStdin();
|
|
@@ -584,16 +732,16 @@ describe('IssueSessionTui', () => {
|
|
|
584
732
|
});
|
|
585
733
|
if (restoredQuestions.length === 1) {
|
|
586
734
|
expect(lastFrame() ?? '').not.toContain('Open questions');
|
|
587
|
-
expect(lastFrame() ?? '').toContain('任意のタイミングで入力 · /exit で終了');
|
|
735
|
+
expect(lastFrame() ?? '').toContain('任意のタイミングで入力 · /reconnect で再接続 · /exit で終了');
|
|
588
736
|
}
|
|
589
737
|
else {
|
|
590
738
|
expect(lastFrame() ?? '').toContain('Open questions');
|
|
591
739
|
expect(lastFrame() ?? '').toContain('inq-resumed-1');
|
|
592
|
-
expect(lastFrame() ?? '').not.toContain('任意のタイミングで入力 · /exit で終了');
|
|
740
|
+
expect(lastFrame() ?? '').not.toContain('任意のタイミングで入力 · /reconnect で再接続 · /exit で終了');
|
|
593
741
|
setOperatorQuestions([]);
|
|
594
742
|
await flushInkStdin();
|
|
595
743
|
expect(lastFrame() ?? '').not.toContain('Open questions');
|
|
596
|
-
expect(lastFrame() ?? '').toContain('任意のタイミングで入力 · /exit で終了');
|
|
744
|
+
expect(lastFrame() ?? '').toContain('任意のタイミングで入力 · /reconnect で再接続 · /exit で終了');
|
|
597
745
|
}
|
|
598
746
|
});
|
|
599
747
|
it('renders all activity log label kinds with distinct markers', () => {
|
|
@@ -632,6 +780,43 @@ describe('IssueSessionTui', () => {
|
|
|
632
780
|
expect(frame).not.toContain('**');
|
|
633
781
|
expect(frame).not.toContain('`');
|
|
634
782
|
});
|
|
783
|
+
it('renders nested Markdown links in pane activity and open questions', () => {
|
|
784
|
+
const viewModel = createTuiViewModel();
|
|
785
|
+
viewModel.setDisplayState({
|
|
786
|
+
workers: {},
|
|
787
|
+
conductorOutput: null,
|
|
788
|
+
openQuestions: [
|
|
789
|
+
createOpenQuestion({
|
|
790
|
+
id: 'inq-link',
|
|
791
|
+
question: '- **choose** [this](https://example.com/question)\n' +
|
|
792
|
+
' - **`deep`**',
|
|
793
|
+
context: 'Use [docs](https://example.com/context).',
|
|
794
|
+
}),
|
|
795
|
+
],
|
|
796
|
+
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
797
|
+
});
|
|
798
|
+
viewModel.appendActivityLog('conductor', '- **bold** [activity](https://example.com/activity)\n - `nested`');
|
|
799
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, issueLinkMode: "osc8", onSubmit: () => { } }));
|
|
800
|
+
const frame = lastFrame() ?? '';
|
|
801
|
+
const visibleFrame = frame.replace(/\u001b\]8;;[^\u0007]*\u0007/g, '');
|
|
802
|
+
expect(visibleFrame).toContain('[conductor]');
|
|
803
|
+
expect(visibleFrame).toContain('- bold activity');
|
|
804
|
+
expect(visibleFrame).toContain('▸ inq-link [text] - choose this');
|
|
805
|
+
expect(visibleFrame).toContain(' Use docs.');
|
|
806
|
+
expect(frame).toContain('\u001b]8;;https://example.com/activity\u0007');
|
|
807
|
+
expect(frame).toContain('\u001b]8;;https://example.com/question\u0007');
|
|
808
|
+
expect(frame).toContain('\u001b]8;;https://example.com/context\u0007');
|
|
809
|
+
expect(frame).not.toContain('**');
|
|
810
|
+
expect(frame).not.toContain('`');
|
|
811
|
+
});
|
|
812
|
+
it('keeps inline link text visible without OSC 8 in URL fallback mode', () => {
|
|
813
|
+
const viewModel = createTuiViewModel();
|
|
814
|
+
viewModel.appendActivityLog('conductor', 'Read [docs](https://example.com/docs).');
|
|
815
|
+
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, issueLinkMode: "url", onSubmit: () => { } }));
|
|
816
|
+
const frame = lastFrame() ?? '';
|
|
817
|
+
expect(frame).toContain('Read docs.');
|
|
818
|
+
expect(frame).not.toContain('\u001b]8;;https://example.com/docs\u0007');
|
|
819
|
+
});
|
|
635
820
|
it('embeds pane titles on top borders without inner title rows', () => {
|
|
636
821
|
const viewModel = createTuiViewModel();
|
|
637
822
|
viewModel.setDisplayState({
|
|
@@ -656,13 +841,15 @@ describe('IssueSessionTui', () => {
|
|
|
656
841
|
expect(lastFrame() ?? '').toContain('Orchestration');
|
|
657
842
|
});
|
|
658
843
|
it('fills orchestration pane when scroll hint is shown on the top border', async () => {
|
|
844
|
+
const terminalRows = 24;
|
|
845
|
+
const liveFrameRows = terminalRows - 1;
|
|
659
846
|
Object.defineProperty(process.stdout, 'columns', {
|
|
660
847
|
configurable: true,
|
|
661
848
|
value: 40,
|
|
662
849
|
});
|
|
663
850
|
Object.defineProperty(process.stdout, 'rows', {
|
|
664
851
|
configurable: true,
|
|
665
|
-
value:
|
|
852
|
+
value: terminalRows,
|
|
666
853
|
});
|
|
667
854
|
const viewModel = createTuiViewModel();
|
|
668
855
|
fillScrollableHarnessLog(viewModel, 50);
|
|
@@ -670,7 +857,7 @@ describe('IssueSessionTui', () => {
|
|
|
670
857
|
stdin.write(INK_TEST_KEYS.pageUp);
|
|
671
858
|
await flushInkStdin();
|
|
672
859
|
const capacity = computeOrchestrationLogVisibleLineCount(computeActivityPaneHeight({
|
|
673
|
-
terminalRows:
|
|
860
|
+
terminalRows: liveFrameRows,
|
|
674
861
|
hintLineCount: 2,
|
|
675
862
|
openQuestionsPaneHeight: 0,
|
|
676
863
|
}));
|
|
@@ -683,6 +870,7 @@ describe('IssueSessionTui', () => {
|
|
|
683
870
|
});
|
|
684
871
|
it('fills orchestration pane log rows without unused inner blank lines', () => {
|
|
685
872
|
for (const terminalRows of [24, 40, 50]) {
|
|
873
|
+
const liveFrameRows = terminalRows - 1;
|
|
686
874
|
Object.defineProperty(process.stdout, 'rows', {
|
|
687
875
|
configurable: true,
|
|
688
876
|
value: terminalRows,
|
|
@@ -691,7 +879,7 @@ describe('IssueSessionTui', () => {
|
|
|
691
879
|
fillScrollableHarnessLog(viewModel, 100);
|
|
692
880
|
const { lastFrame, unmount } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
693
881
|
const capacity = computeActivityLogLineCapacity({
|
|
694
|
-
terminalRows,
|
|
882
|
+
terminalRows: liveFrameRows,
|
|
695
883
|
hintLineCount: 1,
|
|
696
884
|
openQuestionsPaneHeight: 0,
|
|
697
885
|
});
|
|
@@ -766,18 +954,30 @@ describe('IssueSessionTui', () => {
|
|
|
766
954
|
workers: {},
|
|
767
955
|
conductorOutput: null,
|
|
768
956
|
openQuestions: [
|
|
769
|
-
createOpenQuestion({
|
|
770
|
-
|
|
957
|
+
createOpenQuestion({
|
|
958
|
+
id: 'inq-1',
|
|
959
|
+
question: 'First question',
|
|
960
|
+
context: 'first selected detail',
|
|
961
|
+
}),
|
|
962
|
+
createOpenQuestion({
|
|
963
|
+
id: 'inq-2',
|
|
964
|
+
question: 'Second question',
|
|
965
|
+
context: 'second selected detail',
|
|
966
|
+
}),
|
|
771
967
|
],
|
|
772
968
|
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
773
969
|
});
|
|
774
970
|
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
775
971
|
expect(lastFrame() ?? '').toContain('▸ inq-1');
|
|
972
|
+
expect(lastFrame() ?? '').toContain('first selected detail');
|
|
973
|
+
expect(lastFrame() ?? '').not.toContain('second selected detail');
|
|
776
974
|
stdin.write(INK_TEST_KEYS.shiftDownArrow);
|
|
777
975
|
await flushInkStdin();
|
|
778
976
|
const frame = lastFrame() ?? '';
|
|
779
977
|
expect(frame).toContain('2/2');
|
|
780
978
|
expect(frame).toContain('▸ inq-2');
|
|
979
|
+
expect(frame).toContain('second selected detail');
|
|
980
|
+
expect(frame).not.toContain('first selected detail');
|
|
781
981
|
});
|
|
782
982
|
it('does not change selection on plain arrow keys', async () => {
|
|
783
983
|
const viewModel = createTuiViewModel();
|
|
@@ -839,7 +1039,7 @@ describe('IssueSessionTui', () => {
|
|
|
839
1039
|
await flushInkStdin();
|
|
840
1040
|
expect(onSubmit).toHaveBeenCalledWith('follow-up', undefined);
|
|
841
1041
|
});
|
|
842
|
-
it('
|
|
1042
|
+
it('keeps compact rows visible when long selected detail reaches the cap', async () => {
|
|
843
1043
|
const viewModel = createTuiViewModel();
|
|
844
1044
|
viewModel.setDisplayState({
|
|
845
1045
|
workers: {},
|
|
@@ -847,16 +1047,36 @@ describe('IssueSessionTui', () => {
|
|
|
847
1047
|
openQuestions: [
|
|
848
1048
|
createOpenQuestion({
|
|
849
1049
|
id: 'inq-long',
|
|
850
|
-
question: 'word '.repeat(
|
|
851
|
-
context: 'context '.repeat(
|
|
1050
|
+
question: `question-start ${'word '.repeat(100)}`,
|
|
1051
|
+
context: `first-detail-start ${'context '.repeat(80)}context-tail`,
|
|
1052
|
+
}),
|
|
1053
|
+
createOpenQuestion({
|
|
1054
|
+
id: 'inq-compact-1',
|
|
1055
|
+
question: 'Second question',
|
|
1056
|
+
context: 'second-detail',
|
|
1057
|
+
}),
|
|
1058
|
+
createOpenQuestion({
|
|
1059
|
+
id: 'inq-compact-2',
|
|
1060
|
+
question: 'Third question',
|
|
852
1061
|
}),
|
|
853
1062
|
],
|
|
854
1063
|
dispatchHold: { hold: false, heldEventCount: 0 },
|
|
855
1064
|
});
|
|
856
|
-
const { lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
857
|
-
const paneHeight = resolveOpenQuestionsPaneHeight(viewModel.getSnapshot().displayState.openQuestions);
|
|
1065
|
+
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
1066
|
+
const paneHeight = resolveOpenQuestionsPaneHeight(viewModel.getSnapshot().displayState.openQuestions, 23);
|
|
858
1067
|
expect(paneHeight).toBeGreaterThan(OPEN_QUESTIONS_PANE_MIN_HEIGHT);
|
|
859
|
-
|
|
1068
|
+
const initialFrame = lastFrame() ?? '';
|
|
1069
|
+
expect(initialFrame).toContain('first-detail-start');
|
|
1070
|
+
expect(initialFrame).toContain('inq-compact-1');
|
|
1071
|
+
expect(initialFrame).toContain('inq-compact-2');
|
|
1072
|
+
stdin.write(INK_TEST_KEYS.shiftDownArrow);
|
|
1073
|
+
await flushInkStdin();
|
|
1074
|
+
const switchedFrame = lastFrame() ?? '';
|
|
1075
|
+
expect(switchedFrame).toContain('▸ inq-compact-1');
|
|
1076
|
+
expect(switchedFrame).toContain('second-detail');
|
|
1077
|
+
expect(switchedFrame).toContain('inq-long');
|
|
1078
|
+
expect(switchedFrame).toContain('inq-compact-2');
|
|
1079
|
+
expect(switchedFrame).not.toContain('first-detail-start');
|
|
860
1080
|
});
|
|
861
1081
|
});
|
|
862
1082
|
describe('orchestration pane scroll (stdin integration)', () => {
|
|
@@ -888,6 +1108,24 @@ describe('IssueSessionTui', () => {
|
|
|
888
1108
|
expect(restoredFrame).toContain('[harness] line-29');
|
|
889
1109
|
expect(restoredFrame).not.toContain(SCROLL_HINT);
|
|
890
1110
|
});
|
|
1111
|
+
it('keeps the same marker visible across a detached append and follows latest on End', async () => {
|
|
1112
|
+
const viewModel = createTuiViewModel();
|
|
1113
|
+
fillScrollableHarnessLog(viewModel);
|
|
1114
|
+
const { stdin, lastFrame } = render(_jsx(IssueSessionTui, { viewModel: viewModel, onSubmit: () => { } }));
|
|
1115
|
+
stdin.write(INK_TEST_KEYS.pageUp);
|
|
1116
|
+
await flushInkStdin();
|
|
1117
|
+
expect(lastFrame() ?? '').toContain('[harness] line-16');
|
|
1118
|
+
viewModel.appendActivityLog('harness', 'newest-marker');
|
|
1119
|
+
await flushInkStdin();
|
|
1120
|
+
const detachedFrame = lastFrame() ?? '';
|
|
1121
|
+
expect(detachedFrame).toContain('[harness] line-16');
|
|
1122
|
+
expect(detachedFrame).not.toContain('[harness] newest-marker');
|
|
1123
|
+
stdin.write(INK_TEST_KEYS.end);
|
|
1124
|
+
await flushInkStdin();
|
|
1125
|
+
const followedFrame = lastFrame() ?? '';
|
|
1126
|
+
expect(followedFrame).toContain('[harness] newest-marker');
|
|
1127
|
+
expect(followedFrame).toContain('[harness] line-29');
|
|
1128
|
+
});
|
|
891
1129
|
it('does not scroll on plain PgUp when input has text; Ctrl+PgUp scrolls without changing input', async () => {
|
|
892
1130
|
const viewModel = createTuiViewModel();
|
|
893
1131
|
fillScrollableHarnessLog(viewModel);
|