@deepseek-harness-tui/dsh-tui 0.7.1 → 0.7.3

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 (127) hide show
  1. package/README.md +7 -5
  2. package/bin/dsh-tui.js +42 -1
  3. package/cordis.patch.yml +10 -0
  4. package/lib/types/commands.js +1 -1
  5. package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
  6. package/lib/types/components/GoalTodoPanel.js +6 -1
  7. package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
  8. package/lib/types/components/HistorySearchDialog.js +5 -1
  9. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  10. package/lib/types/components/LoadedContextPanel.js +1 -1
  11. package/lib/types/components/MessageList.d.ts.map +1 -1
  12. package/lib/types/components/MessageList.js +5 -1
  13. package/lib/types/components/PromptInput.d.ts.map +1 -1
  14. package/lib/types/components/PromptInput.js +52 -74
  15. package/lib/types/components/SearchBox.d.ts +4 -0
  16. package/lib/types/components/SearchBox.d.ts.map +1 -1
  17. package/lib/types/components/SearchBox.js +101 -2
  18. package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts +4 -1
  19. package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts.map +1 -1
  20. package/lib/types/components/Spinner/SpinnerAnimationRow.js +10 -5
  21. package/lib/types/components/WorkingSpinner.d.ts +3 -1
  22. package/lib/types/components/WorkingSpinner.d.ts.map +1 -1
  23. package/lib/types/components/WorkingSpinner.js +2 -2
  24. package/lib/types/components/design-system/ThemedText.d.ts +9 -3
  25. package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
  26. package/lib/types/components/design-system/ThemedText.js +3 -5
  27. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  28. package/lib/types/components/questions/AskUserQuestionPanel.js +52 -8
  29. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  30. package/lib/types/components/questions/PlanReviewPanel.js +12 -3
  31. package/lib/types/components/sessions/SessionListRow.d.ts +26 -0
  32. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -0
  33. package/lib/types/components/sessions/SessionListRow.js +35 -0
  34. package/lib/types/components/sessions/SessionPreview.d.ts +25 -0
  35. package/lib/types/components/sessions/SessionPreview.d.ts.map +1 -0
  36. package/lib/types/components/sessions/SessionPreview.js +60 -0
  37. package/lib/types/dsh-adapter/channel.d.ts +19 -4
  38. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  39. package/lib/types/dsh-adapter/channel.js +76 -82
  40. package/lib/types/dsh-adapter/compat/index.d.ts +1 -1
  41. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  42. package/lib/types/dsh-adapter/compat/index.js +1 -1
  43. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +8 -0
  44. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  45. package/lib/types/dsh-adapter/compat/sessionLog.js +1 -1
  46. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  47. package/lib/types/dsh-adapter/index.js +10 -1
  48. package/lib/types/dsh-adapter/packaged-presets.d.ts +22 -0
  49. package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -0
  50. package/lib/types/dsh-adapter/packaged-presets.js +93 -0
  51. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  52. package/lib/types/dsh-adapter/plugin.js +53 -7
  53. package/lib/types/dsh-adapter/presets.d.ts +16 -0
  54. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  55. package/lib/types/dsh-adapter/presets.js +23 -0
  56. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  57. package/lib/types/dsh-adapter/providerWizard.js +3 -1
  58. package/lib/types/dsh-adapter/sessions/digest.d.ts +29 -0
  59. package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -0
  60. package/lib/types/dsh-adapter/sessions/digest.js +224 -0
  61. package/lib/types/dsh-adapter/sessions/frames.d.ts +100 -0
  62. package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -0
  63. package/lib/types/dsh-adapter/sessions/frames.js +246 -0
  64. package/lib/types/dsh-adapter/sessions/header.d.ts +50 -0
  65. package/lib/types/dsh-adapter/sessions/header.d.ts.map +1 -0
  66. package/lib/types/dsh-adapter/sessions/header.js +62 -0
  67. package/lib/types/dsh-adapter/sessions/index.d.ts +17 -0
  68. package/lib/types/dsh-adapter/sessions/index.d.ts.map +1 -0
  69. package/lib/types/dsh-adapter/sessions/index.js +15 -0
  70. package/lib/types/dsh-adapter/sessions/list.d.ts +41 -0
  71. package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -0
  72. package/lib/types/dsh-adapter/sessions/list.js +213 -0
  73. package/lib/types/dsh-adapter/sessions/store.d.ts +48 -0
  74. package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -0
  75. package/lib/types/dsh-adapter/sessions/store.js +164 -0
  76. package/lib/types/dsh-adapter/sessions/types.d.ts +142 -0
  77. package/lib/types/dsh-adapter/sessions/types.d.ts.map +1 -0
  78. package/lib/types/dsh-adapter/sessions/types.js +14 -0
  79. package/lib/types/i18n.d.ts +150 -22
  80. package/lib/types/i18n.d.ts.map +1 -1
  81. package/lib/types/i18n.js +45 -9
  82. package/lib/types/ink/components/Text.d.ts +9 -1
  83. package/lib/types/ink/components/Text.d.ts.map +1 -1
  84. package/lib/types/ink/components/Text.js +7 -6
  85. package/lib/types/ink/ink.d.ts.map +1 -1
  86. package/lib/types/ink/ink.js +4 -2
  87. package/lib/types/ink/log-update.d.ts.map +1 -1
  88. package/lib/types/ink/log-update.js +76 -9
  89. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  90. package/lib/types/ink/parse-keypress.js +4 -1
  91. package/lib/types/ink/terminal.d.ts +40 -4
  92. package/lib/types/ink/terminal.d.ts.map +1 -1
  93. package/lib/types/ink/terminal.js +57 -5
  94. package/lib/types/ink/termio/tokenize.d.ts +6 -0
  95. package/lib/types/ink/termio/tokenize.d.ts.map +1 -1
  96. package/lib/types/ink/termio/tokenize.js +18 -4
  97. package/lib/types/screens/Chat.d.ts +3 -2
  98. package/lib/types/screens/Chat.d.ts.map +1 -1
  99. package/lib/types/screens/Chat.js +104 -160
  100. package/lib/types/screens/SessionBrowser.d.ts +37 -0
  101. package/lib/types/screens/SessionBrowser.d.ts.map +1 -0
  102. package/lib/types/screens/SessionBrowser.js +452 -0
  103. package/lib/types/sessionHistory.d.ts +0 -8
  104. package/lib/types/sessionHistory.d.ts.map +1 -1
  105. package/lib/types/sessions/format.d.ts +117 -0
  106. package/lib/types/sessions/format.d.ts.map +1 -0
  107. package/lib/types/sessions/format.js +244 -0
  108. package/lib/types/sessions/view.d.ts +125 -0
  109. package/lib/types/sessions/view.d.ts.map +1 -0
  110. package/lib/types/sessions/view.js +234 -0
  111. package/lib/types/update.d.ts +21 -7
  112. package/lib/types/update.d.ts.map +1 -1
  113. package/lib/types/update.js +66 -16
  114. package/lib/types/utils/paths.d.ts.map +1 -1
  115. package/lib/types/utils/paths.js +4 -1
  116. package/package.json +37 -9
  117. package/presets/liangshen/.dsh-tui-managed.json +5 -0
  118. package/presets/liangshen/agent.cordis.yml +389 -0
  119. package/presets/liangshen/compaction-epoch.mjs +81 -0
  120. package/presets/liangshen/custom-bash.mjs +126 -0
  121. package/presets/liangshen/instruction-hint.mjs +181 -0
  122. package/presets/liangshen/preset.yml +3 -0
  123. package/presets/liangshen/skill-search.mjs +142 -0
  124. package/presets/liangshen/tool-bootstrap.mjs +300 -0
  125. package/lib/types/components/ResumePicker.d.ts +0 -26
  126. package/lib/types/components/ResumePicker.d.ts.map +0 -1
  127. package/lib/types/components/ResumePicker.js +0 -50
@@ -1,6 +1,54 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
2
3
  import Box from '../ink/components/Box.js';
3
4
  import Text from '../ink/components/Text.js';
5
+ import measureElement from '../ink/measure-element.js';
6
+ import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js';
7
+ import { useTerminalSize } from '../ink/hooks/use-terminal-size.js';
8
+ import { stringWidth } from '../ink/stringWidth.js';
9
+ /**
10
+ * Window a single-line query around the caret so the visible slice fits
11
+ * `avail` display cells (CJK display width, code-point safe — `[...s]`
12
+ * iterates code points, not UTF-16 units). The caret character itself
13
+ * always stays inside the window: leading characters are dropped until
14
+ * before-caret text + caret fit, then after-caret text fills what remains.
15
+ * `offset` arrives in UTF-16 units (callers move the caret by key count);
16
+ * a mid-surrogate offset would split an emoji, so it snaps back to the
17
+ * pair's start first.
18
+ */
19
+ function windowQuery(query, offset, avail) {
20
+ const budget = Math.max(avail, 1);
21
+ let caret = Math.max(0, Math.min(offset, query.length));
22
+ if (caret > 0 &&
23
+ caret < query.length &&
24
+ query.charCodeAt(caret - 1) >= 0xd800 &&
25
+ query.charCodeAt(caret - 1) <= 0xdbff &&
26
+ query.charCodeAt(caret) >= 0xdc00 &&
27
+ query.charCodeAt(caret) <= 0xdfff) {
28
+ caret--; // mid-surrogate: snap to the emoji's start
29
+ }
30
+ const beforeChars = [...query.slice(0, caret)];
31
+ const at = caret < query.length ? [...query.slice(caret)][0] : ' ';
32
+ const atWidth = Math.max(1, stringWidth(at));
33
+ let caretColumn = 0;
34
+ for (const ch of beforeChars)
35
+ caretColumn += stringWidth(ch);
36
+ let start = 0;
37
+ while (start < beforeChars.length && caretColumn + atWidth > budget) {
38
+ caretColumn -= stringWidth(beforeChars[start]);
39
+ start++;
40
+ }
41
+ let rest = budget - caretColumn - atWidth;
42
+ let after = '';
43
+ for (const ch of [...query.slice(caret + at.length)]) {
44
+ const w = stringWidth(ch);
45
+ if (w > rest)
46
+ break;
47
+ after += ch;
48
+ rest -= w;
49
+ }
50
+ return { before: beforeChars.slice(start).join(''), at, after, caretColumn };
51
+ }
4
52
  /**
5
53
  * A single-line search input in the round-bordered box of Claude Code's
6
54
  * SearchBox: `⌕ ` prefix, block cursor at `cursorOffset` (inverse cell).
@@ -8,6 +56,10 @@ import Text from '../ink/components/Text.js';
8
56
  * placeholder is right-aligned (dimmed) — kept off the caret's cell so the
9
57
  * terminal-painted IME preedit (pinyin) can never be overlaid on it during
10
58
  * CJK composition.
59
+ *
60
+ * The query row is strictly single-line: an overlong query is windowed
61
+ * around the caret (horizontal scroll) instead of wrapping, so the native
62
+ * cursor declaration below stays exact for any query length.
11
63
  */
12
64
  export function SearchBox({ query, placeholder = 'Search…', isFocused, isTerminalFocused, prefix = '⌕', width, cursorOffset, borderless = false, }) {
13
65
  const offset = cursorOffset ?? query.length;
@@ -17,10 +69,57 @@ export function SearchBox({ query, placeholder = 'Search…', isFocused, isTermi
17
69
  // Focused + empty + terminal focused: inline caret row (block caret at the
18
70
  // start, placeholder right-aligned) instead of the inline placeholder.
19
71
  const inlineCaret = isFocused && query === '' && isTerminalFocused;
72
+ // Content width of the box in display cells. Measured from yoga after
73
+ // layout (resize re-layouts without any prop/state change, so measure on
74
+ // every commit — the setState is a no-op when the width is unchanged);
75
+ // the terminal-columns estimate only covers the very first frame.
76
+ const { columns } = useTerminalSize();
77
+ const chrome = borderless ? 0 : 4; // 2 border cells + paddingX 2
78
+ const [measuredWidth, setMeasuredWidth] = useState(null);
79
+ const contentWidth = measuredWidth ?? Math.max(8, columns - chrome - 2);
80
+ const prefixWidth = stringWidth(`${prefix} `);
81
+ const win = windowQuery(query, offset, contentWidth - prefixWidth);
82
+ // Park the native terminal cursor at the caret so IME preedit (pinyin)
83
+ // renders inline at the input instead of the screen's bottom row (same
84
+ // mechanism as PromptInput). The declaration hangs on the outer Box —
85
+ // content stays a single Text (splitting it into flex siblings
86
+ // reorders/drops glyphs on narrow widths), so the position is computed:
87
+ // border (1) + paddingX (1) per edge when bordered, then the `prefix `
88
+ // run and the windowed before-caret text, all in display cells.
89
+ const showCaret = isFocused && isTerminalFocused;
90
+ // Clamp into the box's content area: on absurdly narrow layouts the
91
+ // prefix alone can meet or exceed the content width, and the park must
92
+ // never land outside the box's rect.
93
+ const edge = borderless ? 0 : 2;
94
+ const maxColumn = edge + Math.max(0, contentWidth - 1);
95
+ const caretColumn = Math.min(edge + prefixWidth + win.caretColumn, maxColumn);
96
+ const declarationRef = useDeclaredCursor({
97
+ line: borderless ? 0 : 1,
98
+ column: caretColumn,
99
+ active: showCaret,
100
+ });
101
+ const boxNodeRef = useRef(null);
102
+ const boxRef = useCallback((node) => {
103
+ boxNodeRef.current = node;
104
+ declarationRef(node);
105
+ }, [declarationRef]);
106
+ useLayoutEffect(() => {
107
+ const node = boxNodeRef.current;
108
+ if (!node)
109
+ return;
110
+ // Layout runs before layout effects (reconciler resetAfterCommit), so a
111
+ // zero raw width means a genuinely zero-width box — but a zero CONTENT
112
+ // width (chrome eats the whole box) is a real, must-clamp case.
113
+ const raw = measureElement(node).width;
114
+ if (raw > 0) {
115
+ const w = Math.max(0, raw - chrome);
116
+ setMeasuredWidth(prev => (prev === w ? prev : w));
117
+ }
118
+ });
20
119
  let content;
21
120
  if (isFocused) {
22
121
  if (query) {
23
- content = isTerminalFocused ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: query.slice(0, offset) }), _jsx(Text, { inverse: true, children: offset < query.length ? query[offset] : ' ' }), offset < query.length && _jsx(Text, { children: query.slice(offset + 1) })] })) : (_jsx(Text, { children: query }));
122
+ content = isTerminalFocused ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: win.before }), _jsx(Text, { inverse: true, children: win.at }), win.after !== '' && _jsx(Text, { children: win.after })] })) : (_jsx(Text, { children: query }));
24
123
  }
25
124
  else if (!isTerminalFocused) {
26
125
  content = _jsx(Text, { dimColor: true, children: placeholder });
@@ -29,5 +128,5 @@ export function SearchBox({ query, placeholder = 'Search…', isFocused, isTermi
29
128
  else {
30
129
  content = query ? _jsx(Text, { children: query }) : _jsx(Text, { children: placeholder });
31
130
  }
32
- return (_jsx(Box, { flexShrink: 0, borderStyle: borderStyle, borderColor: borderColor, borderDimColor: borderDimColor, paddingX: borderless ? 0 : 1, width: width, children: inlineCaret ? (_jsxs(Box, { flexDirection: "row", width: "100%", children: [_jsxs(Text, { children: [prefix, " "] }), _jsx(Text, { inverse: true, children: " " }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, wrap: "truncate", children: placeholder })] })) : (_jsxs(Text, { dimColor: !isFocused, children: [prefix, " ", content] })) }));
131
+ return (_jsx(Box, { ref: boxRef, flexShrink: 0, borderStyle: borderStyle, borderColor: borderColor, borderDimColor: borderDimColor, paddingX: borderless ? 0 : 1, width: width, children: inlineCaret ? (_jsxs(Box, { flexDirection: "row", width: "100%", children: [_jsxs(Text, { children: [prefix, " "] }), _jsx(Text, { inverse: true, children: " " }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, wrap: "truncate", children: placeholder })] })) : (_jsxs(Text, { dimColor: !isFocused, wrap: "truncate-end", children: [prefix, " ", content] })) }));
33
132
  }
@@ -7,6 +7,9 @@ export type SpinnerAnimationRowProps = {
7
7
  hasActiveTools: boolean;
8
8
  /** Raw response length (chars) — feeds the animated token counter. */
9
9
  responseLengthRef: React.RefObject<number>;
10
+ /** Most recent request's real upload tokens (input + cache read/write);
11
+ * 0 until the first usage event lands. */
12
+ uploadTokensRef: React.RefObject<number>;
10
13
  /** Stable within a turn. */
11
14
  message: string;
12
15
  messageColor: keyof Theme;
@@ -27,5 +30,5 @@ export type SpinnerAnimationRowProps = {
27
30
  * animation clock (frame, glimmer, token counter animation, elapsed time,
28
31
  * stalled intensity, thinking shimmer).
29
32
  */
30
- export declare function SpinnerAnimationRow({ mode, reducedMotion, hasActiveTools, responseLengthRef, message, messageColor, shimmerColor, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, spinnerSuffix, verbose, columns, thinkingStatus, }: SpinnerAnimationRowProps): React.ReactNode;
33
+ export declare function SpinnerAnimationRow({ mode, reducedMotion, hasActiveTools, responseLengthRef, uploadTokensRef, message, messageColor, shimmerColor, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, spinnerSuffix, verbose, columns, thinkingStatus, }: SpinnerAnimationRowProps): React.ReactNode;
31
34
  //# sourceMappingURL=SpinnerAnimationRow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SpinnerAnimationRow.d.ts","sourceRoot":"","sources":["../../../../src/components/Spinner/SpinnerAnimationRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAA0B,MAAM,OAAO,CAAA;AAM9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAI3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAcnD,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,WAAW,CAAA;IACjB,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,sEAAsE;IACtE,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC1C,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,KAAK,CAAA;IACzB,YAAY,EAAE,MAAM,KAAK,CAAA;IACzB,mBAAmB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC5C,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACzC,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACjD,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,cAAc,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,OAAO,EACP,OAAO,EACP,cAAc,GACf,EAAE,wBAAwB,GAAG,KAAK,CAAC,SAAS,CAwM5C"}
1
+ {"version":3,"file":"SpinnerAnimationRow.d.ts","sourceRoot":"","sources":["../../../../src/components/Spinner/SpinnerAnimationRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAA0B,MAAM,OAAO,CAAA;AAM9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAI3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAcnD,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,WAAW,CAAA;IACjB,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,sEAAsE;IACtE,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC1C;+CAC2C;IAC3C,eAAe,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACxC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,KAAK,CAAA;IACzB,YAAY,EAAE,MAAM,KAAK,CAAA;IACzB,mBAAmB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC5C,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACzC,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACjD,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,cAAc,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,OAAO,EACP,OAAO,EACP,cAAc,GACf,EAAE,wBAAwB,GAAG,KAAK,CAAC,SAAS,CA6M5C"}
@@ -26,7 +26,7 @@ const THINKING_GLOW_PERIOD_S = 2;
26
26
  * animation clock (frame, glimmer, token counter animation, elapsed time,
27
27
  * stalled intensity, thinking shimmer).
28
28
  */
29
- export function SpinnerAnimationRow({ mode, reducedMotion, hasActiveTools, responseLengthRef, message, messageColor, shimmerColor, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, spinnerSuffix, verbose, columns, thinkingStatus, }) {
29
+ export function SpinnerAnimationRow({ mode, reducedMotion, hasActiveTools, responseLengthRef, uploadTokensRef, message, messageColor, shimmerColor, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, spinnerSuffix, verbose, columns, thinkingStatus, }) {
30
30
  const [viewportRef, time] = useAnimationFrame(reducedMotion ? null : 50);
31
31
  // === Elapsed time (wall-clock, derived from refs each frame) ===
32
32
  const now = Date.now();
@@ -81,8 +81,13 @@ export function SpinnerAnimationRow({ mode, reducedMotion, hasActiveTools, respo
81
81
  const timerText = formatDuration(elapsedTimeMs);
82
82
  const timerWidth = stringWidth(timerText);
83
83
  const tokenCount = formatNumber(leaderTokens);
84
- const tokensText = `${figures.arrowDown} ${tokenCount} tokens`;
85
- const tokensWidth = stringWidth(tokensText);
84
+ const uploadTokens = uploadTokensRef.current;
85
+ // Real upload tokens (last request's input + cache) ride beside the
86
+ // animated download estimate; both labeled once to keep the row short.
87
+ const tokensLabel = uploadTokens > 0
88
+ ? `↑ ${formatNumber(uploadTokens)} · ↓ ${tokenCount} tokens`
89
+ : `↓ ${tokenCount} tokens`;
90
+ const tokensWidth = stringWidth(tokensLabel);
86
91
  // === Thinking text (may shrink to fit) ===
87
92
  let thinkingText = thinkingStatus === 'thinking'
88
93
  ? 'thinking'
@@ -108,7 +113,7 @@ export function SpinnerAnimationRow({ mode, reducedMotion, hasActiveTools, respo
108
113
  const showTimer = wantsTimerAndTokens && availableSpace > usedAfterThinking + timerWidth;
109
114
  const usedAfterTimer = usedAfterThinking + (showTimer ? timerWidth + sep : 0);
110
115
  const showTokens = wantsTimerAndTokens &&
111
- leaderTokens > 0 &&
116
+ (leaderTokens > 0 || uploadTokens > 0) &&
112
117
  availableSpace > usedAfterTimer + tokensWidth;
113
118
  const thinkingOnly = showThinking &&
114
119
  thinkingStatus === 'thinking' &&
@@ -137,7 +142,7 @@ export function SpinnerAnimationRow({ mode, reducedMotion, hasActiveTools, respo
137
142
  : []),
138
143
  ...(showTokens
139
144
  ? [
140
- _jsxs(Box, { flexDirection: "row", children: [_jsx(SpinnerModeGlyph, { mode: mode }), _jsxs(Text, { dimColor: true, children: [tokenCount, " tokens"] })] }, "tokens"),
145
+ _jsxs(Box, { flexDirection: "row", children: [_jsx(SpinnerModeGlyph, { mode: mode }), _jsx(Text, { dimColor: true, children: tokensLabel })] }, "tokens"),
141
146
  ]
142
147
  : []),
143
148
  ...(showThinking && thinkingText
@@ -8,10 +8,12 @@ import type { SpinnerMode } from './Spinner/spinnerMode.js';
8
8
  *
9
9
  * Random verb is picked once per turn (per mount of the spinner).
10
10
  */
11
- export declare function WorkingSpinner({ mode, hasActiveTools, responseLengthRef, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, thinkingStatus, }: {
11
+ export declare function WorkingSpinner({ mode, hasActiveTools, responseLengthRef, uploadTokensRef, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, thinkingStatus, }: {
12
12
  mode: SpinnerMode;
13
13
  hasActiveTools: boolean;
14
14
  responseLengthRef: React.RefObject<number>;
15
+ /** Most recent request's real upload tokens; 0 until the first usage event. */
16
+ uploadTokensRef: React.RefObject<number>;
15
17
  loadingStartTimeRef: React.RefObject<number>;
16
18
  totalPausedMsRef: React.RefObject<number>;
17
19
  pauseStartTimeRef: React.RefObject<number | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"WorkingSpinner.d.ts","sourceRoot":"","sources":["../../../src/components/WorkingSpinner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAA;AAG1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAK3D;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,GACf,EAAE;IACD,IAAI,EAAE,WAAW,CAAA;IACjB,cAAc,EAAE,OAAO,CAAA;IACvB,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC1C,mBAAmB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC5C,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACzC,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACjD,cAAc,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;CAC3C,GAAG,KAAK,CAAC,SAAS,CA2BlB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,OAAO,GAClB,UAAU,GAAG,MAAM,GAAG,IAAI,CA6C5B"}
1
+ {"version":3,"file":"WorkingSpinner.d.ts","sourceRoot":"","sources":["../../../src/components/WorkingSpinner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAA;AAG1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAK3D;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,GACf,EAAE;IACD,IAAI,EAAE,WAAW,CAAA;IACjB,cAAc,EAAE,OAAO,CAAA;IACvB,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC1C,+EAA+E;IAC/E,eAAe,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACxC,mBAAmB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC5C,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IACzC,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACjD,cAAc,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;CAC3C,GAAG,KAAK,CAAC,SAAS,CA4BlB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,OAAO,GAClB,UAAU,GAAG,MAAM,GAAG,IAAI,CA6C5B"}
@@ -13,12 +13,12 @@ import { sample } from 'lodash-es';
13
13
  *
14
14
  * Random verb is picked once per turn (per mount of the spinner).
15
15
  */
16
- export function WorkingSpinner({ mode, hasActiveTools, responseLengthRef, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, thinkingStatus, }) {
16
+ export function WorkingSpinner({ mode, hasActiveTools, responseLengthRef, uploadTokensRef, loadingStartTimeRef, totalPausedMsRef, pauseStartTimeRef, thinkingStatus, }) {
17
17
  const { columns } = useTerminalSize();
18
18
  // Pick a random verb once per spinner mount (per turn).
19
19
  const [randomVerb] = useState(() => sample(SPINNER_VERBS) ?? 'Working');
20
20
  const message = `${randomVerb}…`;
21
- return (_jsx(Box, { flexDirection: "column", width: "100%", alignItems: "flex-start", children: _jsx(SpinnerAnimationRow, { mode: mode, reducedMotion: false, hasActiveTools: hasActiveTools, responseLengthRef: responseLengthRef, message: message, messageColor: "claude", shimmerColor: "claudeShimmer", loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, spinnerSuffix: null, verbose: true, columns: columns, thinkingStatus: thinkingStatus }) }));
21
+ return (_jsx(Box, { flexDirection: "column", width: "100%", alignItems: "flex-start", children: _jsx(SpinnerAnimationRow, { mode: mode, reducedMotion: false, hasActiveTools: hasActiveTools, responseLengthRef: responseLengthRef, uploadTokensRef: uploadTokensRef, message: message, messageColor: "claude", shimmerColor: "claudeShimmer", loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, spinnerSuffix: null, verbose: true, columns: columns, thinkingStatus: thinkingStatus }) }));
22
22
  }
23
23
  /**
24
24
  * Tracks thinking status: 'thinking' while the model is streaming reasoning,
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { Color, Styles } from '../../ink/styles.js';
3
+ import type { DOMElement } from '../../ink/dom.js';
3
4
  import { type Theme } from '../../theme.js';
4
5
  /**
5
6
  * Colors uncolored ThemedText in the subtree. Precedence: explicit `color` >
@@ -13,9 +14,9 @@ export type Props = {
13
14
  */
14
15
  readonly color?: keyof Theme | Color;
15
16
  /**
16
- * Same as `color`, but for background. Must be a theme key.
17
+ * Same as `color`, but for background.
17
18
  */
18
- readonly backgroundColor?: keyof Theme;
19
+ readonly backgroundColor?: keyof Theme | Color;
19
20
  /**
20
21
  * Dim the color using the theme's inactive color.
21
22
  * This is compatible with bold (unlike ANSI dim).
@@ -45,6 +46,11 @@ export type Props = {
45
46
  * This property tells Ink to wrap or truncate text if its width is larger than container.
46
47
  */
47
48
  readonly wrap?: Styles['textWrap'];
49
+ /**
50
+ * Ref to the underlying ink-text DOMElement (e.g. useDeclaredCursor
51
+ * parking the native cursor on an inline caret cell).
52
+ */
53
+ readonly ref?: React.Ref<DOMElement>;
48
54
  readonly children?: React.ReactNode;
49
55
  };
50
56
  /**
@@ -52,5 +58,5 @@ export type Props = {
52
58
  * (in the Claude Code visual language). This is what lets every ported CC
53
59
  * component use `color="subtle"`-style theme keys unchanged.
54
60
  */
55
- export default function ThemedText({ color, backgroundColor, dimColor, bold, italic, underline, strikethrough, inverse, wrap, children, }: Props): React.ReactNode;
61
+ export default function ThemedText({ color, backgroundColor, dimColor, bold, italic, underline, strikethrough, inverse, wrap, ref, children, }: Props): React.ReactNode;
56
62
  //# sourceMappingURL=ThemedText.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ThemedText.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/ThemedText.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EAAY,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAGrD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,wCAEtB,CAAA;AAqBZ,MAAM,MAAM,KAAK,GAAG;IAClB;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,GAAG,KAAK,CAAA;IAEpC;;OAEG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,CAAA;IAEtC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAE3B;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IAEvB;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IAEzB;;OAEG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAE5B;;OAEG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;IAEhC;;OAEG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAE1B;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;IAElC,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACpC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EACjC,KAAK,EACL,eAAe,EACf,QAAgB,EAChB,IAAY,EACZ,MAAc,EACd,SAAiB,EACjB,aAAqB,EACrB,OAAe,EACf,IAAa,EACb,QAAQ,GACT,EAAE,KAAK,GAAG,KAAK,CAAC,SAAS,CA8BzB"}
1
+ {"version":3,"file":"ThemedText.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/ThemedText.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAY,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAGrD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,wCAEtB,CAAA;AAqBZ,MAAM,MAAM,KAAK,GAAG;IAClB;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,GAAG,KAAK,CAAA;IAEpC;;OAEG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,GAAG,KAAK,CAAA;IAE9C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAE3B;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IAEvB;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IAEzB;;OAEG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAE5B;;OAEG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;IAEhC;;OAEG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAE1B;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;IAElC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IAEpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACpC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EACjC,KAAK,EACL,eAAe,EACf,QAAgB,EAChB,IAAY,EACZ,MAAc,EACd,SAAiB,EACjB,aAAqB,EACrB,OAAe,EACf,IAAa,EACb,GAAG,EACH,QAAQ,GACT,EAAE,KAAK,GAAG,KAAK,CAAC,SAAS,CA6BzB"}
@@ -28,7 +28,7 @@ function resolveColor(color, theme) {
28
28
  * (in the Claude Code visual language). This is what lets every ported CC
29
29
  * component use `color="subtle"`-style theme keys unchanged.
30
30
  */
31
- export default function ThemedText({ color, backgroundColor, dimColor = false, bold = false, italic = false, underline = false, strikethrough = false, inverse = false, wrap = 'wrap', children, }) {
31
+ export default function ThemedText({ color, backgroundColor, dimColor = false, bold = false, italic = false, underline = false, strikethrough = false, inverse = false, wrap = 'wrap', ref, children, }) {
32
32
  const [themeName] = useTheme();
33
33
  const theme = getTheme(themeName);
34
34
  const hoverColor = React.useContext(TextHoverColorContext);
@@ -38,8 +38,6 @@ export default function ThemedText({ color, backgroundColor, dimColor = false, b
38
38
  : dimColor
39
39
  ? theme.inactive
40
40
  : resolveColor(color, theme);
41
- const resolvedBackgroundColor = backgroundColor
42
- ? theme[backgroundColor]
43
- : undefined;
44
- return (_jsx(Text, { color: resolvedColor, backgroundColor: resolvedBackgroundColor, bold: bold, italic: italic, underline: underline, strikethrough: strikethrough, inverse: inverse, wrap: wrap, children: children }));
41
+ const resolvedBackgroundColor = resolveColor(backgroundColor, theme);
42
+ return (_jsx(Text, { ref: ref, color: resolvedColor, backgroundColor: resolvedBackgroundColor, bold: bold, italic: italic, underline: underline, strikethrough: strikethrough, inverse: inverse, wrap: wrap, children: children }));
45
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AskUserQuestionPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/AskUserQuestionPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAQvE,MAAM,MAAM,yBAAyB,GAAG;IACtC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;QAC9B;;oEAE4D;QAC5D,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAA;QAClC;0EACkE;QAClE,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAC7E,CAAA;IACD,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACzD,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,yBAAyB,GAAG,KAAK,CAAC,SAAS,CAqT7C"}
1
+ {"version":3,"file":"AskUserQuestionPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/AskUserQuestionPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AASvE,MAAM,MAAM,yBAAyB,GAAG;IACtC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;QAC9B;;oEAE4D;QAC5D,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAA;QAClC;0EACkE;QAClE,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAC7E,CAAA;IACD,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACzD,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,yBAAyB,GAAG,KAAK,CAAC,SAAS,CAwW7C"}
@@ -14,11 +14,13 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
14
14
  */
15
15
  import React from 'react';
16
16
  import { t } from '../../i18n.js';
17
- import { Box, Text, useInput } from '../../ui.js';
17
+ import { Box, Text, useInput, useTerminalSize } from '../../ui.js';
18
+ import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js';
18
19
  import { Divider } from '../design-system/Divider.js';
19
20
  import { POINTER } from '../../cc/figures.js';
20
21
  import { PlanReviewPanel } from './PlanReviewPanel.js';
21
22
  import { isPlainReturnInput } from '../../utils/modifiers.js';
23
+ import { listWindow } from '../listWindow.js';
22
24
  const CHECKED = '◉';
23
25
  const UNCHECKED = '○';
24
26
  const PENCIL = '✎';
@@ -32,6 +34,7 @@ export function AskUserQuestionPanel({ question, position, total, answered, onAn
32
34
  const options = question.options ?? [];
33
35
  const multiSelect = question.multiSelect === true;
34
36
  const hideCustomInput = question.hideCustomInput === true && options.length > 0;
37
+ const { rows: terminalRows } = useTerminalSize();
35
38
  /** Rows: the real options plus the inline input row at the tail. */
36
39
  const rowCount = options.length + (hideCustomInput ? 0 : 1);
37
40
  const [focusIndex, setFocusIndex] = React.useState(0);
@@ -43,6 +46,35 @@ export function AskUserQuestionPanel({ question, position, total, answered, onAn
43
46
  const [attached, setAttached] = React.useState(null);
44
47
  const [error, setError] = React.useState(null);
45
48
  const inputFocused = !hideCustomInput && focusIndex === options.length;
49
+ // Chat chrome + panel scaffolding consume twelve rows before the option
50
+ // list: status line, outer/divider/question/list/hint spacing and content.
51
+ // Optional header/detail/input/error rows are charged explicitly. Long
52
+ // lists then use fixed one/two-line rows so listWindow's budget is exact;
53
+ // short questionnaires retain their existing wrapped presentation.
54
+ const detailRows = question.detail === undefined ? 0 : question.detail.split('\n').length + 1;
55
+ const reservedRows = 12
56
+ + (question.header === undefined ? 0 : 1)
57
+ + detailRows
58
+ + (hideCustomInput ? 0 : 1)
59
+ + (error === null ? 0 : 2);
60
+ const optionBudget = Math.max(terminalRows - reservedRows, 2);
61
+ const optionHeights = options.map(option => option.description === undefined ? 1 : 2);
62
+ const windowedOptions = optionHeights.reduce((sum, height) => sum + height, 0) > optionBudget;
63
+ const optionFocus = Math.min(focusIndex, Math.max(options.length - 1, 0));
64
+ const optionWindow = windowedOptions
65
+ ? listWindow(optionHeights, optionFocus, optionBudget)
66
+ : { start: 0, end: options.length };
67
+ // Park the native terminal cursor on the custom-answer caret: terminal
68
+ // emulators render IME preedit (pinyin) at the physical cursor, so without
69
+ // this declaration CJK composition appears at the screen's bottom row
70
+ // instead of inline at the input (same mechanism as PromptInput's value
71
+ // box). Active whenever the input row is visible — typing on an option row
72
+ // also lands in this input, so the IME anchor must follow even when the
73
+ // row itself is not focused. The ref rides on the caret Text itself (all
74
+ // three visual variants): its nodeCache rect IS the caret cell, so (0, 0)
75
+ // stays exact under CJK widths and line wrapping without any
76
+ // layout-affecting wrapper Box.
77
+ const caretRef = useDeclaredCursor({ line: 0, column: 0, active: !hideCustomInput });
46
78
  const moveFocus = (delta) => {
47
79
  if (rowCount <= 1)
48
80
  return;
@@ -209,13 +241,25 @@ export function AskUserQuestionPanel({ question, position, total, answered, onAn
209
241
  const remaining = total - answered;
210
242
  const headerTitle = ` ${t('question-header-progress', { position, total, remaining: remaining > 1 ? t('question-remaining-more', { n: remaining }) : '' })} `;
211
243
  const cursorChar = customCursor < customText.length ? customText[customCursor] : ' ';
212
- const renderInputRow = () => (_jsxs(Box, { flexDirection: "row", marginTop: inputFocused ? 1 : 0, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: inputFocused ? 'claude' : undefined, bold: inputFocused, children: inputFocused ? POINTER : ' ' }) }), _jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: inputFocused ? 'claude' : 'suggestion', children: PENCIL }) }), _jsxs(Box, { flexDirection: "row", marginLeft: 1, children: [_jsx(Text, { bold: inputFocused, color: inputFocused ? 'claude' : 'suggestion', children: t('question-custom-tab') }), attached !== null && (_jsx(Text, { color: "suggestion", children: t('question-attached-label', { label: attached }) })), _jsx(Text, { dimColor: true, children: "\uFF1A" }), customText === '' && !inputFocused ? (_jsx(Text, { dimColor: true, children: t('question-direct-input') })) : (_jsxs(_Fragment, { children: [_jsx(Text, { wrap: "wrap", children: customText.slice(0, customCursor) }), inputFocused
213
- ? _jsx(Text, { inverse: true, children: cursorChar })
214
- : _jsx(Text, { color: "suggestion", children: "\u258F" }), _jsx(Text, { wrap: "wrap", children: customText.slice(inputFocused ? customCursor + 1 : customCursor) })] }))] })] }));
215
- const renderOptions = () => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.map((option, index) => {
216
- const focused = index === focusIndex;
217
- const selected = multiSelect ? checked.has(index) : focused;
218
- return (_jsxs(Box, { flexDirection: "row", marginTop: focused ? 1 : 0, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: focused ? 'claude' : undefined, bold: focused, children: focused ? POINTER : ' ' }) }), _jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: focused ? 'claude' : undefined, bold: selected, children: selected ? (multiSelect ? CHECKED : '●') : UNCHECKED }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsx(Text, { bold: focused || selected, color: focused ? 'claude' : undefined, wrap: "wrap", children: option.label }), option.description !== undefined && (_jsx(Text, { dimColor: true, wrap: "wrap", children: option.description }))] })] }, option.label));
244
+ const renderInputRow = () => (_jsxs(Box, { flexDirection: "row", marginTop: inputFocused ? 1 : 0, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: inputFocused ? 'claude' : undefined, bold: inputFocused, children: inputFocused ? POINTER : ' ' }) }), _jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: inputFocused ? 'claude' : 'suggestion', children: PENCIL }) }), _jsxs(Box, { flexDirection: "row", marginLeft: 1, children: [_jsx(Text, { bold: inputFocused, color: inputFocused ? 'claude' : 'suggestion', children: t('question-custom-tab') }), attached !== null && (_jsx(Text, { color: "suggestion", children: t('question-attached-label', { label: attached }) })), _jsx(Text, { dimColor: true, children: "\uFF1A" }), customText === '' && !inputFocused ? (_jsx(Text, { ref: caretRef, dimColor: true, children: t('question-direct-input') })) : (_jsxs(_Fragment, { children: [_jsx(Text, { wrap: "wrap", children: customText.slice(0, customCursor) }), inputFocused
245
+ ? _jsx(Text, { ref: caretRef, inverse: true, children: cursorChar })
246
+ : _jsx(Text, { ref: caretRef, color: "suggestion", children: "\u258F" }), _jsx(Text, { wrap: "wrap", children: customText.slice(inputFocused ? customCursor + 1 : customCursor) })] }))] })] }));
247
+ const renderOptions = () => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.slice(optionWindow.start, optionWindow.end).map((option, index) => {
248
+ const absoluteIndex = optionWindow.start + index;
249
+ const focused = absoluteIndex === focusIndex;
250
+ const selected = multiSelect ? checked.has(absoluteIndex) : focused;
251
+ const pointer = focused
252
+ ? POINTER
253
+ : absoluteIndex === optionWindow.start && optionWindow.start > 0
254
+ ? '↑'
255
+ : absoluteIndex === optionWindow.end - 1 && optionWindow.end < options.length
256
+ ? '↓'
257
+ : ' ';
258
+ const label = windowedOptions ? option.label.replace(/\s+/gu, ' ').trim() : option.label;
259
+ const description = windowedOptions
260
+ ? option.description?.replace(/\s+/gu, ' ').trim()
261
+ : option.description;
262
+ return (_jsxs(Box, { flexDirection: "row", marginTop: !windowedOptions && focused ? 1 : 0, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: focused ? 'claude' : undefined, bold: focused, children: pointer }) }), _jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: focused ? 'claude' : undefined, bold: selected, children: selected ? (multiSelect ? CHECKED : '●') : UNCHECKED }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsx(Text, { bold: focused || selected, color: focused ? 'claude' : undefined, wrap: windowedOptions ? 'truncate' : 'wrap', children: label }), description !== undefined && (_jsx(Text, { dimColor: true, wrap: windowedOptions ? 'truncate' : 'wrap', children: description }))] })] }, `${absoluteIndex}:${option.label}`));
219
263
  }), hideCustomInput ? null : renderInputRow()] }));
220
264
  const hintParts = inputFocused
221
265
  ? [
@@ -1 +1 @@
1
- {"version":3,"file":"PlanReviewPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/PlanReviewPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAKvE,MAAM,MAAM,oBAAoB,GAAG;IACjC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAC7E,CAAA;IACD,QAAQ,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACzD,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,GAAG,KAAK,CAAC,SAAS,CAgOxC"}
1
+ {"version":3,"file":"PlanReviewPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/PlanReviewPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAKvE,MAAM,MAAM,oBAAoB,GAAG;IACjC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAC7E,CAAA;IACD,QAAQ,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACzD,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,GAAG,KAAK,CAAC,SAAS,CAyOxC"}
@@ -18,6 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
18
18
  import React from 'react';
19
19
  import { t } from '../../i18n.js';
20
20
  import { Box, Text, useInput } from '../../ui.js';
21
+ import { useDeclaredCursor } from '../../ink/hooks/use-declared-cursor.js';
21
22
  import { Divider } from '../design-system/Divider.js';
22
23
  import { Markdown } from '../Markdown.js';
23
24
  import { POINTER } from '../../cc/figures.js';
@@ -34,6 +35,14 @@ export function PlanReviewPanel({ question, onAnswer, onCancel, }) {
34
35
  const [cursor, setCursor] = React.useState(0);
35
36
  const [error, setError] = React.useState(null);
36
37
  const inputFocused = focusIndex === options.length;
38
+ // Park the native terminal cursor on the feedback caret so IME preedit
39
+ // (pinyin) renders inline at the input instead of the screen's bottom row
40
+ // (same mechanism as AskUserQuestionPanel / PromptInput). Always active —
41
+ // typing on an option row also lands in the feedback buffer. The ref rides
42
+ // on the caret Text itself (all visual variants): its nodeCache rect IS
43
+ // the caret cell, so (0, 0) stays exact under wrapping without a
44
+ // layout-affecting wrapper Box.
45
+ const caretRef = useDeclaredCursor({ line: 0, column: 0, active: true });
37
46
  const moveFocus = (delta) => {
38
47
  setFocusIndex(index => (index + delta + rowCount) % rowCount);
39
48
  setError(null);
@@ -163,7 +172,7 @@ export function PlanReviewPanel({ question, onAnswer, onCancel, }) {
163
172
  const focused = index === focusIndex;
164
173
  const isApprove = option.label === approveLabel;
165
174
  return (_jsxs(Box, { flexDirection: "row", marginTop: focused ? 1 : 0, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: focused ? 'claude' : undefined, bold: focused, children: focused ? POINTER : ' ' }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { bold: focused, color: focused || isApprove ? 'claude' : undefined, wrap: "wrap", children: [index + 1, ". ", option.label] }), option.description !== undefined && (_jsx(Text, { dimColor: true, wrap: "wrap", children: option.description }))] })] }, option.label));
166
- }), _jsxs(Box, { flexDirection: "row", marginTop: inputFocused ? 1 : 0, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: inputFocused ? 'claude' : undefined, bold: inputFocused, children: inputFocused ? POINTER : ' ' }) }), _jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: inputFocused ? 'claude' : 'suggestion', children: PENCIL }) }), _jsx(Box, { flexDirection: "row", marginLeft: 1, children: feedback === '' && !inputFocused ? (_jsx(Text, { dimColor: true, children: t('plan-review-feedback-placeholder') })) : (_jsxs(_Fragment, { children: [_jsx(Text, { wrap: "wrap", children: feedback.slice(0, cursor) }), inputFocused
167
- ? _jsx(Text, { inverse: true, children: cursorChar })
168
- : _jsx(Text, { color: "suggestion", children: "\u258F" }), _jsx(Text, { wrap: "wrap", children: feedback.slice(inputFocused ? cursor + 1 : cursor) })] })) })] })] }), error !== null && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "error", children: error }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('plan-review-hint') }) })] }));
175
+ }), _jsxs(Box, { flexDirection: "row", marginTop: inputFocused ? 1 : 0, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: inputFocused ? 'claude' : undefined, bold: inputFocused, children: inputFocused ? POINTER : ' ' }) }), _jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: inputFocused ? 'claude' : 'suggestion', children: PENCIL }) }), _jsx(Box, { flexDirection: "row", marginLeft: 1, children: feedback === '' && !inputFocused ? (_jsx(Text, { ref: caretRef, dimColor: true, children: t('plan-review-feedback-placeholder') })) : (_jsxs(_Fragment, { children: [_jsx(Text, { wrap: "wrap", children: feedback.slice(0, cursor) }), inputFocused
176
+ ? _jsx(Text, { ref: caretRef, inverse: true, children: cursorChar })
177
+ : _jsx(Text, { ref: caretRef, color: "suggestion", children: "\u258F" }), _jsx(Text, { wrap: "wrap", children: feedback.slice(inputFocused ? cursor + 1 : cursor) })] })) })] })] }), error !== null && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "error", children: error }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('plan-review-hint') }) })] }));
169
178
  }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import type { SessionSummary } from '../../dsh-adapter/sessions/index.js';
3
+ /**
4
+ * One session in the browser's list: a title line and a metadata line.
5
+ *
6
+ * Two lines rather than one because the two carry different jobs. The title
7
+ * answers "is this the conversation I mean"; the metadata answers "which of
8
+ * the three that look alike is it" — when it was, on what branch, how big,
9
+ * under which model. Folding both onto one line makes the title compete with
10
+ * facts nobody reads first, and on a narrow terminal the title is what loses.
11
+ *
12
+ * Widths are resolved here rather than delegated to flexbox: the row must
13
+ * stay exactly two lines at every terminal width, and a row that wraps
14
+ * destroys the alignment that lets the eye scan a list at all.
15
+ */
16
+ export declare function SessionListRow({ session, width, depth, focused, now, }: {
17
+ session: SessionSummary;
18
+ /** Columns available to the row, indentation included. */
19
+ width: number;
20
+ /** 0 for a conversation, 1 for a sub-agent run under its parent. */
21
+ depth: number;
22
+ focused: boolean;
23
+ /** Epoch ms used for every relative time in this render pass. */
24
+ now: number;
25
+ }): React.ReactNode;
26
+ //# sourceMappingURL=SessionListRow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionListRow.d.ts","sourceRoot":"","sources":["../../../../src/components/sessions/SessionListRow.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAA;AAEzE;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,KAAK,EACL,KAAK,EACL,OAAO,EACP,GAAG,GACJ,EAAE;IACD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,KAAK,CAAC,SAAS,CAoClB"}
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from '../../ui.js';
3
+ import { t } from '../../i18n.js';
4
+ import { formatBytes, formatWhen, kindMark, titleColor, truncateWidth, } from '../../sessions/format.js';
5
+ /**
6
+ * One session in the browser's list: a title line and a metadata line.
7
+ *
8
+ * Two lines rather than one because the two carry different jobs. The title
9
+ * answers "is this the conversation I mean"; the metadata answers "which of
10
+ * the three that look alike is it" — when it was, on what branch, how big,
11
+ * under which model. Folding both onto one line makes the title compete with
12
+ * facts nobody reads first, and on a narrow terminal the title is what loses.
13
+ *
14
+ * Widths are resolved here rather than delegated to flexbox: the row must
15
+ * stay exactly two lines at every terminal width, and a row that wraps
16
+ * destroys the alignment that lets the eye scan a list at all.
17
+ */
18
+ export function SessionListRow({ session, width, depth, focused, now, }) {
19
+ const indent = depth * 2;
20
+ // Two cells for the focus marker, plus the indent for a nested run.
21
+ const body = Math.max(8, width - 2 - indent);
22
+ const mark = kindMark(session.kind);
23
+ const facts = [formatWhen(session.updatedAt, now)];
24
+ if (session.branch !== undefined)
25
+ facts.push(session.branch);
26
+ const size = formatBytes(session.bytes);
27
+ if (size !== undefined)
28
+ facts.push(size);
29
+ if (session.model !== undefined)
30
+ facts.push(session.model);
31
+ if (session.childCount > 0 && depth === 0) {
32
+ facts.push(t('session-children', { n: session.childCount }));
33
+ }
34
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: focused ? 'suggestion' : 'subtle', children: `${' '.repeat(indent)}${focused ? '❯ ' : ' '}` }), mark !== undefined && _jsx(Text, { color: mark.color, children: `${mark.glyph} ` }), _jsx(Text, { color: titleColor(session.title.source, focused), bold: focused, children: truncateWidth(session.label ?? session.title.text, body - (mark === undefined ? 0 : 2)) })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `${' '.repeat(indent + 2)}${truncateWidth(facts.join(' · '), body)}` }) })] }));
35
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import type { PreviewEntry, SessionSummary } from '../../dsh-adapter/sessions/index.js';
3
+ /**
4
+ * The preview pane: what this session actually says.
5
+ *
6
+ * It shows the END of the conversation, not the beginning, for two reasons.
7
+ * The title already carries the beginning — it is usually the first prompt —
8
+ * so repeating it would spend the pane on something the list already said. And
9
+ * the question a person asks at this moment is "is this the one I was in the
10
+ * middle of", which only the last exchange answers.
11
+ *
12
+ * It is also the reason the pane costs nothing: the tail of a log is exactly
13
+ * what a bounded read already has in hand, so opening the preview on a 4 MB
14
+ * session is the same amount of work as on a 40 KB one.
15
+ */
16
+ export declare function SessionPreview({ session, entries, loading, width, height, home, now, }: {
17
+ session: SessionSummary;
18
+ entries: readonly PreviewEntry[];
19
+ loading: boolean;
20
+ width: number;
21
+ height: number;
22
+ home: string;
23
+ now: number;
24
+ }): React.ReactNode;
25
+ //# sourceMappingURL=SessionPreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionPreview.d.ts","sourceRoot":"","sources":["../../../../src/components/sessions/SessionPreview.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAA;AAQvF;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,OAAO,EACP,OAAO,EACP,KAAK,EACL,MAAM,EACN,IAAI,EACJ,GAAG,GACJ,EAAE;IACD,OAAO,EAAE,cAAc,CAAA;IACvB,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,KAAK,CAAC,SAAS,CA8DlB"}