@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.
Files changed (86) hide show
  1. package/README.md +36 -3
  2. package/config.example.yaml +13 -0
  3. package/dist/async-operator-input.js +1 -1
  4. package/dist/async-operator-input.js.map +1 -1
  5. package/dist/display/session-display-reducer.d.ts.map +1 -1
  6. package/dist/display/session-display-reducer.js +0 -11
  7. package/dist/display/session-display-reducer.js.map +1 -1
  8. package/dist/inline-markdown.d.ts +3 -1
  9. package/dist/inline-markdown.d.ts.map +1 -1
  10. package/dist/inline-markdown.js +232 -33
  11. package/dist/inline-markdown.js.map +1 -1
  12. package/dist/issue-command.d.ts.map +1 -1
  13. package/dist/issue-command.js +8 -4
  14. package/dist/issue-command.js.map +1 -1
  15. package/dist/prompt-operator-input.d.ts +1 -1
  16. package/dist/prompt-operator-input.d.ts.map +1 -1
  17. package/dist/prompt-operator-input.js +1 -1
  18. package/dist/prompt-operator-input.js.map +1 -1
  19. package/dist/session-log-lines.d.ts.map +1 -1
  20. package/dist/session-log-lines.js +64 -8
  21. package/dist/session-log-lines.js.map +1 -1
  22. package/dist/session-sinks.d.ts.map +1 -1
  23. package/dist/session-sinks.js +2 -10
  24. package/dist/session-sinks.js.map +1 -1
  25. package/dist/tui/activity-log.d.ts +8 -0
  26. package/dist/tui/activity-log.d.ts.map +1 -1
  27. package/dist/tui/activity-log.js +16 -0
  28. package/dist/tui/activity-log.js.map +1 -1
  29. package/dist/tui/compute-operator-input-cursor-y.d.ts +4 -0
  30. package/dist/tui/compute-operator-input-cursor-y.d.ts.map +1 -1
  31. package/dist/tui/compute-operator-input-cursor-y.js +4 -2
  32. package/dist/tui/compute-operator-input-cursor-y.js.map +1 -1
  33. package/dist/tui/create-issue-session-tui-host.d.ts.map +1 -1
  34. package/dist/tui/create-issue-session-tui-host.js +44 -8
  35. package/dist/tui/create-issue-session-tui-host.js.map +1 -1
  36. package/dist/tui/create-issue-session-tui-host.test.js +19 -5
  37. package/dist/tui/create-issue-session-tui-host.test.js.map +1 -1
  38. package/dist/tui/issue-session-tui-stream.d.ts +6 -1
  39. package/dist/tui/issue-session-tui-stream.d.ts.map +1 -1
  40. package/dist/tui/issue-session-tui-stream.js +55 -17
  41. package/dist/tui/issue-session-tui-stream.js.map +1 -1
  42. package/dist/tui/issue-session-tui-stream.test.js +345 -11
  43. package/dist/tui/issue-session-tui-stream.test.js.map +1 -1
  44. package/dist/tui/issue-session-tui.d.ts +11 -5
  45. package/dist/tui/issue-session-tui.d.ts.map +1 -1
  46. package/dist/tui/issue-session-tui.js +60 -30
  47. package/dist/tui/issue-session-tui.js.map +1 -1
  48. package/dist/tui/issue-session-tui.test.js +268 -30
  49. package/dist/tui/issue-session-tui.test.js.map +1 -1
  50. package/dist/tui/open-questions-pane.d.ts.map +1 -1
  51. package/dist/tui/open-questions-pane.js +30 -12
  52. package/dist/tui/open-questions-pane.js.map +1 -1
  53. package/dist/tui/pane-layout.d.ts +22 -0
  54. package/dist/tui/pane-layout.d.ts.map +1 -0
  55. package/dist/tui/pane-layout.js +74 -0
  56. package/dist/tui/pane-layout.js.map +1 -0
  57. package/dist/tui/stream-scrollback.d.ts +29 -0
  58. package/dist/tui/stream-scrollback.d.ts.map +1 -0
  59. package/dist/tui/stream-scrollback.js +35 -0
  60. package/dist/tui/stream-scrollback.js.map +1 -0
  61. package/dist/tui/titled-border-pane.d.ts +2 -1
  62. package/dist/tui/titled-border-pane.d.ts.map +1 -1
  63. package/dist/tui/titled-border-pane.js +2 -2
  64. package/dist/tui/titled-border-pane.js.map +1 -1
  65. package/dist/tui/tui-layout-constants.d.ts +11 -6
  66. package/dist/tui/tui-layout-constants.d.ts.map +1 -1
  67. package/dist/tui/tui-layout-constants.js +11 -6
  68. package/dist/tui/tui-layout-constants.js.map +1 -1
  69. package/dist/tui/tui-resize-recovery.d.ts +24 -0
  70. package/dist/tui/tui-resize-recovery.d.ts.map +1 -0
  71. package/dist/tui/tui-resize-recovery.js +34 -0
  72. package/dist/tui/tui-resize-recovery.js.map +1 -0
  73. package/dist/tui/tui-terminal-size.d.ts +45 -0
  74. package/dist/tui/tui-terminal-size.d.ts.map +1 -0
  75. package/dist/tui/tui-terminal-size.js +195 -0
  76. package/dist/tui/tui-terminal-size.js.map +1 -0
  77. package/dist/tui/tui-view-model.d.ts +2 -0
  78. package/dist/tui/tui-view-model.d.ts.map +1 -1
  79. package/dist/tui/tui-view-model.js +7 -0
  80. package/dist/tui/tui-view-model.js.map +1 -1
  81. package/dist/tui/wrap-text-to-width.d.ts.map +1 -1
  82. package/dist/tui/wrap-text-to-width.js +4 -1
  83. package/dist/tui/wrap-text-to-width.js.map +1 -1
  84. package/docs/config.md +67 -3
  85. package/docs/settings.md +17 -4
  86. 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 { INPUT_PANE_TITLE, MAIN_PANE_TITLE, OPEN_QUESTIONS_PANE_MIN_HEIGHT, OPERATOR_INPUT_CURSOR_Y_OFFSET, PANE_PADDING_X, ROUND_BORDER_WIDTH, WORKER_PANE_TITLE, } from './tui-layout-constants.js';
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(terminalRows);
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(terminalRows);
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(terminalRows);
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: 24,
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: 24,
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({ id: 'inq-1', question: 'First question' }),
770
- createOpenQuestion({ id: 'inq-2', question: 'Second question' }),
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('grows open questions pane for long selected question text', () => {
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(30),
851
- context: 'context '.repeat(10),
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
- expect(lastFrame() ?? '').toContain('context context');
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);