@deepseek-harness-tui/dsh-tui 0.8.7 → 0.9.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 (229) hide show
  1. package/README.md +29 -10
  2. package/bin/dsh-tui.js +1 -1
  3. package/lib/types/commands.d.ts.map +1 -1
  4. package/lib/types/commands.js +4 -2
  5. package/lib/types/components/ActivityPicker.d.ts +4 -1
  6. package/lib/types/components/ActivityPicker.d.ts.map +1 -1
  7. package/lib/types/components/ActivityPicker.js +2 -2
  8. package/lib/types/components/BtwPanel.d.ts.map +1 -1
  9. package/lib/types/components/BtwPanel.js +1 -1
  10. package/lib/types/components/Chat/SubagentMessage.d.ts +2 -1
  11. package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -1
  12. package/lib/types/components/Chat/SubagentMessage.js +11 -3
  13. package/lib/types/components/CommandSuggestions.d.ts +21 -4
  14. package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
  15. package/lib/types/components/CommandSuggestions.js +41 -14
  16. package/lib/types/components/EffortSlider.d.ts +4 -1
  17. package/lib/types/components/EffortSlider.d.ts.map +1 -1
  18. package/lib/types/components/EffortSlider.js +4 -2
  19. package/lib/types/components/ExtensionDialog.js +6 -2
  20. package/lib/types/components/FileSuggestions.d.ts +26 -5
  21. package/lib/types/components/FileSuggestions.d.ts.map +1 -1
  22. package/lib/types/components/FileSuggestions.js +53 -16
  23. package/lib/types/components/GoalTodoPanel.d.ts +26 -2
  24. package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
  25. package/lib/types/components/GoalTodoPanel.js +86 -17
  26. package/lib/types/components/HelpMenu.d.ts +7 -1
  27. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  28. package/lib/types/components/HelpMenu.js +12 -4
  29. package/lib/types/components/HistorySearchDialog.d.ts +4 -1
  30. package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
  31. package/lib/types/components/HistorySearchDialog.js +2 -2
  32. package/lib/types/components/LangPicker.d.ts +14 -0
  33. package/lib/types/components/LangPicker.d.ts.map +1 -0
  34. package/lib/types/components/LangPicker.js +17 -0
  35. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  36. package/lib/types/components/LoadedContextPanel.js +3 -1
  37. package/lib/types/components/LogoV2.d.ts +5 -1
  38. package/lib/types/components/LogoV2.d.ts.map +1 -1
  39. package/lib/types/components/LogoV2.js +12 -3
  40. package/lib/types/components/Markdown.d.ts.map +1 -1
  41. package/lib/types/components/Markdown.js +12 -3
  42. package/lib/types/components/MessageList.d.ts +43 -2
  43. package/lib/types/components/MessageList.d.ts.map +1 -1
  44. package/lib/types/components/MessageList.js +553 -41
  45. package/lib/types/components/ModelPicker.d.ts +4 -1
  46. package/lib/types/components/ModelPicker.d.ts.map +1 -1
  47. package/lib/types/components/ModelPicker.js +2 -2
  48. package/lib/types/components/PermissionsPicker.d.ts +21 -0
  49. package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
  50. package/lib/types/components/PermissionsPicker.js +27 -0
  51. package/lib/types/components/PlanPicker.d.ts +15 -0
  52. package/lib/types/components/PlanPicker.d.ts.map +1 -0
  53. package/lib/types/components/PlanPicker.js +18 -0
  54. package/lib/types/components/PresetPicker.d.ts +4 -1
  55. package/lib/types/components/PresetPicker.d.ts.map +1 -1
  56. package/lib/types/components/PresetPicker.js +2 -2
  57. package/lib/types/components/PromptInput.d.ts +4 -3
  58. package/lib/types/components/PromptInput.d.ts.map +1 -1
  59. package/lib/types/components/PromptInput.js +273 -50
  60. package/lib/types/components/RewindPicker.d.ts +12 -1
  61. package/lib/types/components/RewindPicker.d.ts.map +1 -1
  62. package/lib/types/components/RewindPicker.js +6 -4
  63. package/lib/types/components/ScrollbarGutter.d.ts +28 -0
  64. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
  65. package/lib/types/components/ScrollbarGutter.js +68 -0
  66. package/lib/types/components/Select.d.ts +8 -1
  67. package/lib/types/components/Select.d.ts.map +1 -1
  68. package/lib/types/components/Select.js +4 -2
  69. package/lib/types/components/SkillsPicker.d.ts +4 -1
  70. package/lib/types/components/SkillsPicker.d.ts.map +1 -1
  71. package/lib/types/components/SkillsPicker.js +2 -2
  72. package/lib/types/components/SubagentCard.d.ts +2 -1
  73. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  74. package/lib/types/components/SubagentCard.js +6 -1
  75. package/lib/types/components/SubagentDashboard.d.ts +4 -0
  76. package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
  77. package/lib/types/components/SubagentDashboard.js +9 -1
  78. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
  79. package/lib/types/components/SubagentDetailScene.js +5 -4
  80. package/lib/types/components/SuggestionCard.d.ts +63 -0
  81. package/lib/types/components/SuggestionCard.d.ts.map +1 -0
  82. package/lib/types/components/SuggestionCard.js +77 -0
  83. package/lib/types/components/ThemePicker.d.ts +4 -1
  84. package/lib/types/components/ThemePicker.d.ts.map +1 -1
  85. package/lib/types/components/ThemePicker.js +2 -2
  86. package/lib/types/components/ThinkingToggle.d.ts +4 -1
  87. package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
  88. package/lib/types/components/ThinkingToggle.js +2 -2
  89. package/lib/types/components/TimelineRail.d.ts +53 -0
  90. package/lib/types/components/TimelineRail.d.ts.map +1 -0
  91. package/lib/types/components/TimelineRail.js +190 -0
  92. package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
  93. package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
  94. package/lib/types/components/WorkspaceFlowPicker.js +4 -2
  95. package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
  96. package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
  97. package/lib/types/components/WorkspaceMenuPicker.js +19 -0
  98. package/lib/types/components/WorkspacePicker.d.ts +4 -1
  99. package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
  100. package/lib/types/components/WorkspacePicker.js +2 -2
  101. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
  102. package/lib/types/components/approvals/ApprovalPanel.js +5 -1
  103. package/lib/types/components/design-system/ListItem.d.ts +8 -1
  104. package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
  105. package/lib/types/components/design-system/ListItem.js +8 -3
  106. package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
  107. package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
  108. package/lib/types/components/messages/AssistantTextMessage.js +6 -2
  109. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
  110. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  111. package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
  112. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
  113. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  114. package/lib/types/components/messages/AssistantToolUseMessage.js +4 -2
  115. package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
  116. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  117. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  118. package/lib/types/components/questions/AskUserQuestionPanel.js +34 -2
  119. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  120. package/lib/types/components/questions/PlanReviewPanel.js +13 -2
  121. package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
  122. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  123. package/lib/types/components/sessions/SessionListRow.js +4 -2
  124. package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
  125. package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
  126. package/lib/types/components/trajectory/HotspotView.js +8 -2
  127. package/lib/types/components/trajectory/Ledger.d.ts +9 -1
  128. package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
  129. package/lib/types/components/trajectory/Ledger.js +23 -5
  130. package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
  131. package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
  132. package/lib/types/components/trajectory/WaveBand.js +7 -2
  133. package/lib/types/dsh-adapter/channel.d.ts +27 -6
  134. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  135. package/lib/types/dsh-adapter/channel.js +432 -66
  136. package/lib/types/dsh-adapter/contract.d.ts +58 -18
  137. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  138. package/lib/types/dsh-adapter/contract.js +124 -36
  139. package/lib/types/dsh-adapter/index.d.ts +10 -2
  140. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  141. package/lib/types/dsh-adapter/index.js +10 -13
  142. package/lib/types/dsh-adapter/plugin.d.ts +0 -9
  143. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  144. package/lib/types/dsh-adapter/plugin.js +136 -6
  145. package/lib/types/i18n.d.ts +194 -16
  146. package/lib/types/i18n.d.ts.map +1 -1
  147. package/lib/types/i18n.js +73 -10
  148. package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
  149. package/lib/types/ink/components/AlternateScreen.js +13 -1
  150. package/lib/types/ink/components/App.d.ts +12 -1
  151. package/lib/types/ink/components/App.d.ts.map +1 -1
  152. package/lib/types/ink/components/App.js +101 -4
  153. package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
  154. package/lib/types/ink/components/ClockContext.js +2 -0
  155. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  156. package/lib/types/ink/components/ScrollBox.js +170 -91
  157. package/lib/types/ink/constants.d.ts +9 -0
  158. package/lib/types/ink/constants.d.ts.map +1 -1
  159. package/lib/types/ink/constants.js +9 -0
  160. package/lib/types/ink/events/click-event.d.ts +7 -16
  161. package/lib/types/ink/events/click-event.d.ts.map +1 -1
  162. package/lib/types/ink/events/click-event.js +16 -19
  163. package/lib/types/ink/events/event-handlers.d.ts +11 -1
  164. package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
  165. package/lib/types/ink/events/event-handlers.js +2 -0
  166. package/lib/types/ink/events/input-event.d.ts +9 -0
  167. package/lib/types/ink/events/input-event.d.ts.map +1 -1
  168. package/lib/types/ink/events/input-event.js +4 -0
  169. package/lib/types/ink/events/pointer-event.d.ts +61 -0
  170. package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
  171. package/lib/types/ink/events/pointer-event.js +61 -0
  172. package/lib/types/ink/events/wheel-event.d.ts +17 -0
  173. package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
  174. package/lib/types/ink/events/wheel-event.js +20 -0
  175. package/lib/types/ink/geometry-trace.d.ts +51 -0
  176. package/lib/types/ink/geometry-trace.d.ts.map +1 -0
  177. package/lib/types/ink/geometry-trace.js +83 -0
  178. package/lib/types/ink/hit-test.d.ts +56 -1
  179. package/lib/types/ink/hit-test.d.ts.map +1 -1
  180. package/lib/types/ink/hit-test.js +146 -8
  181. package/lib/types/ink/ink.d.ts +58 -1
  182. package/lib/types/ink/ink.d.ts.map +1 -1
  183. package/lib/types/ink/ink.js +268 -52
  184. package/lib/types/ink/node-cache.d.ts +5 -0
  185. package/lib/types/ink/node-cache.d.ts.map +1 -1
  186. package/lib/types/ink/parse-keypress.d.ts +8 -0
  187. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  188. package/lib/types/ink/parse-keypress.js +101 -12
  189. package/lib/types/ink/render-node-to-output.d.ts +16 -3
  190. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  191. package/lib/types/ink/render-node-to-output.js +144 -25
  192. package/lib/types/ink/selection.d.ts +29 -4
  193. package/lib/types/ink/selection.d.ts.map +1 -1
  194. package/lib/types/ink/selection.js +41 -5
  195. package/lib/types/ink/timeline-rail.d.ts +138 -0
  196. package/lib/types/ink/timeline-rail.d.ts.map +1 -0
  197. package/lib/types/ink/timeline-rail.js +179 -0
  198. package/lib/types/ink/wrap-text.d.ts.map +1 -1
  199. package/lib/types/ink/wrap-text.js +47 -5
  200. package/lib/types/screens/Chat.d.ts.map +1 -1
  201. package/lib/types/screens/Chat.js +819 -337
  202. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  203. package/lib/types/screens/SessionBrowser.js +56 -29
  204. package/lib/types/screens/Settings.d.ts.map +1 -1
  205. package/lib/types/screens/Settings.js +64 -22
  206. package/lib/types/screens/StatusLine.d.ts +2 -1
  207. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  208. package/lib/types/screens/StatusLine.js +17 -1
  209. package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
  210. package/lib/types/screens/TrajectoryScene.js +81 -14
  211. package/lib/types/screens/chatOverlay.d.ts +221 -0
  212. package/lib/types/screens/chatOverlay.d.ts.map +1 -0
  213. package/lib/types/screens/chatOverlay.js +114 -0
  214. package/lib/types/tips.d.ts.map +1 -1
  215. package/lib/types/tips.js +64 -10
  216. package/lib/types/tuiDisplayPrefs.d.ts +15 -0
  217. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  218. package/lib/types/tuiDisplayPrefs.js +9 -0
  219. package/lib/types/utils/debug.d.ts +10 -0
  220. package/lib/types/utils/debug.d.ts.map +1 -1
  221. package/lib/types/utils/debug.js +24 -0
  222. package/lib/types/utils/fileSuggestions.d.ts +19 -0
  223. package/lib/types/utils/fileSuggestions.d.ts.map +1 -0
  224. package/lib/types/utils/fileSuggestions.js +40 -0
  225. package/lib/types/utils/mentions.d.ts.map +1 -1
  226. package/lib/types/utils/mentions.js +3 -1
  227. package/lib/types/utils/paths.d.ts.map +1 -1
  228. package/lib/types/utils/paths.js +1 -0
  229. package/package.json +69 -66
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Timeline rail — the pure geometry model behind the fullscreen
3
+ * transcript's turn navigator (a port of Grok Build's timeline sidebar
4
+ * semantics onto the dsh Ink tree).
5
+ *
6
+ * The rail is a 2-column gutter REPLACING the classic scrollbar: one tick
7
+ * per user turn, where tick position encodes CONVERSATION ORDER, not
8
+ * scroll proportion. Geometry is computed into a single frozen structure
9
+ * consumed by both the renderer and mouse hit-testing, so what you see is
10
+ * always what you can click ("看得到但点不中" impossible by construction).
11
+ *
12
+ * Viewport semantics (all derived from the same turn-top list, mirroring
13
+ * grok-pager's ScrollbackState):
14
+ *
15
+ * - active: the LAST turn whose prompt top is at-or-above the viewport
16
+ * top (the turn whose content owns the top row — "the turn being
17
+ * read"). When pre-turn content (logo / loaded-context panel) owns the
18
+ * top, the FIRST turn stands in. Never the newest-turn clamp: its
19
+ * one-step-off-bottom highlight leap is exactly what this rule avoids.
20
+ * - up target: the last turn STRICTLY above the viewport top. From
21
+ * mid-turn it first aligns the current turn's own prompt, and it can
22
+ * never name a trailing turn no scroll could bring to the top
23
+ * (the stuck-▲ bug).
24
+ * - down target: the FIRST turn whose prompt top is strictly below the
25
+ * viewport top AND reachable (top ≤ maxScroll — the renderer clamps
26
+ * scrollTop to maxScroll, so a turn below it could never own the top
27
+ * row; naming it would make ▼ repeat itself forever).
28
+ *
29
+ * Not ported (deliberately): ratatui Buffer drawing and Rust state
30
+ * organization — algorithms only, per the reference report.
31
+ */
32
+ /** Columns the rail reserves (widest tick glyph). */
33
+ export declare const RAIL_WIDTH = 2;
34
+ /** Terminals narrower than this hide the rail (transcript needs the cols). */
35
+ export declare const RAIL_MIN_TERMINAL_WIDTH = 60;
36
+ /** Fewer turns than this and the rail is noise. */
37
+ export declare const RAIL_MIN_TURNS = 2;
38
+ /** Stored preview cap (chars). Render paths re-truncate to card width. */
39
+ export declare const PREVIEW_MAX_CHARS = 120;
40
+ /** One user turn, as the rail sees it. Identity is the row id — never an
41
+ * array index (deletion / rewind / loadOlder would mis-target it). */
42
+ export interface TimelineTurn {
43
+ /** ChatRow id of the user message (jump/preview target). */
44
+ id: number;
45
+ /** Content-space top of the prompt TEXT (scrollTo(target) pins it to
46
+ * the viewport top). */
47
+ top: number;
48
+ /** First non-empty prompt line, char-capped (see clipPreview). */
49
+ preview: string;
50
+ /**
51
+ * True while the row sits BEFORE the fold window (older than the most
52
+ * recent MAX_RENDERED_ROWS rows): its top is unknown (unmounted since
53
+ * before the fold; `top` carries −1) and clicking the tick must first
54
+ * reveal the folded history (Chat: showAll + force-mount + seek) rather
55
+ * than scrollTo(−1). Rendering a tick for it is still correct — the
56
+ * turn EXISTS and is navigable, it is just folded away right now
57
+ * (drawing only window turns read as "the rail covers just 2-3 nodes"
58
+ * on tool-heavy sessions where 300 rows ≈ a handful of turns).
59
+ */
60
+ folded?: boolean;
61
+ }
62
+ /** The per-commit snapshot MessageList reports and the rail + sticky
63
+ * header consume — one source so the two can never disagree. */
64
+ export interface TimelineSnapshot {
65
+ /** Turns in conversation order. */
66
+ turns: ReadonlyArray<TimelineTurn>;
67
+ /** Turn owning the viewport top row; first turn while pre-turn content
68
+ * owns the top; null only when there are no turns. */
69
+ activeId: number | null;
70
+ /** ▲ target (strictly above the top), null at the first turn. */
71
+ upId: number | null;
72
+ /** ▼ target (below the top and reachable), null at the end. */
73
+ downId: number | null;
74
+ }
75
+ /** Per-render rail geometry: where the ticks and chevrons landed. */
76
+ export interface RailGeometry {
77
+ /** Turn-index window shown as ticks: [windowStart, windowEnd). */
78
+ windowStart: number;
79
+ windowEnd: number;
80
+ /** Row of the ▲ chevron (block-local, within the rail column). */
81
+ upRow: number;
82
+ /** Row of the first tick. */
83
+ tickTop: number;
84
+ /** Row of the ▼ chevron. */
85
+ downRow: number;
86
+ }
87
+ /** What a rail row is. */
88
+ export type RailHit = {
89
+ kind: 'up';
90
+ } | {
91
+ kind: 'down';
92
+ } | {
93
+ kind: 'tick';
94
+ index: number;
95
+ };
96
+ /** Single eligibility policy: setting, terminal width, turn count, and
97
+ * geometric feasibility in one place. */
98
+ export declare function railEligible(opts: {
99
+ turnCount: number;
100
+ terminalWidth: number;
101
+ viewportRows: number;
102
+ /** False when the content fits the viewport (inline mode / fresh
103
+ * session): nothing to navigate, the terminal's own scrollback owns
104
+ * the wheel. */
105
+ scrollable: boolean;
106
+ }): boolean;
107
+ /**
108
+ * Compute rail geometry, or null when the rail must not render.
109
+ *
110
+ * Windowing: when turns outnumber tick rows (viewport − 2 chevrons), slide
111
+ * a window around the ACTIVE turn; at the bottom prefer the tail so the
112
+ * newest ticks stay visible — but never exclude the active turn, or no
113
+ * tick would highlight. The chevron+tick block is vertically centered in
114
+ * the rail column.
115
+ */
116
+ export declare function computeRailGeometry(turnCount: number, viewportRows: number, activeIndex: number | null, atBottom: boolean): RailGeometry | null;
117
+ /**
118
+ * Which rail interaction a block-local row lands on. The whole rail width
119
+ * is the hit target (no pixel-hunting the glyph).
120
+ */
121
+ export declare function railHit(geo: RailGeometry, row: number): RailHit | null;
122
+ /**
123
+ * First non-empty line, char-capped with a `…` marker. Bounded work: the
124
+ * scan stops at the first non-empty line and slices at the cap, so a huge
125
+ * one-line prompt costs O(cap), not O(line length). Char cap (not display
126
+ * width) on purpose — this bounds the stored snapshot; the hover card
127
+ * re-wraps to its own width (wrapPreviewLines).
128
+ */
129
+ export declare function clipPreview(text: string, maxChars?: number): string;
130
+ /**
131
+ * Wrap a preview to at most 2 lines by DISPLAY width (CJK-aware — a
132
+ * terminal cell column budget, not a char count). The last line is
133
+ * ellipsized when content remains. Char-by-char accumulation keeps
134
+ * surrogate pairs and (best-effort) combining sequences intact by never
135
+ * splitting below a low surrogate.
136
+ */
137
+ export declare function wrapPreviewLines(preview: string, maxWidth: number): string[];
138
+ //# sourceMappingURL=timeline-rail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline-rail.d.ts","sourceRoot":"","sources":["../../../src/ink/timeline-rail.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,qDAAqD;AACrD,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC,mDAAmD;AACnD,eAAO,MAAM,cAAc,IAAI,CAAA;AAE/B,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,MAAM,CAAA;AAEpC;uEACuE;AACvE,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,EAAE,EAAE,MAAM,CAAA;IACV;6BACyB;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAA;IACf;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;iEACiE;AACjE,MAAM,WAAW,gBAAgB;IAC/B,mCAAmC;IACnC,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;IAClC;2DACuD;IACvD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,iEAAiE;IACjE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;IACb,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,0BAA0B;AAC1B,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACd;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnC;0CAC0C;AAC1C,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB;;qBAEiB;IACjB,UAAU,EAAE,OAAO,CAAA;CACpB,GAAG,OAAO,CAOV;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,QAAQ,EAAE,OAAO,GAChB,YAAY,GAAG,IAAI,CAuBrB;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAUtE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAoB,GAAG,MAAM,CAkB9E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CA+B5E"}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Timeline rail — the pure geometry model behind the fullscreen
3
+ * transcript's turn navigator (a port of Grok Build's timeline sidebar
4
+ * semantics onto the dsh Ink tree).
5
+ *
6
+ * The rail is a 2-column gutter REPLACING the classic scrollbar: one tick
7
+ * per user turn, where tick position encodes CONVERSATION ORDER, not
8
+ * scroll proportion. Geometry is computed into a single frozen structure
9
+ * consumed by both the renderer and mouse hit-testing, so what you see is
10
+ * always what you can click ("看得到但点不中" impossible by construction).
11
+ *
12
+ * Viewport semantics (all derived from the same turn-top list, mirroring
13
+ * grok-pager's ScrollbackState):
14
+ *
15
+ * - active: the LAST turn whose prompt top is at-or-above the viewport
16
+ * top (the turn whose content owns the top row — "the turn being
17
+ * read"). When pre-turn content (logo / loaded-context panel) owns the
18
+ * top, the FIRST turn stands in. Never the newest-turn clamp: its
19
+ * one-step-off-bottom highlight leap is exactly what this rule avoids.
20
+ * - up target: the last turn STRICTLY above the viewport top. From
21
+ * mid-turn it first aligns the current turn's own prompt, and it can
22
+ * never name a trailing turn no scroll could bring to the top
23
+ * (the stuck-▲ bug).
24
+ * - down target: the FIRST turn whose prompt top is strictly below the
25
+ * viewport top AND reachable (top ≤ maxScroll — the renderer clamps
26
+ * scrollTop to maxScroll, so a turn below it could never own the top
27
+ * row; naming it would make ▼ repeat itself forever).
28
+ *
29
+ * Not ported (deliberately): ratatui Buffer drawing and Rust state
30
+ * organization — algorithms only, per the reference report.
31
+ */
32
+ import { stringWidth } from './stringWidth.js';
33
+ /** Columns the rail reserves (widest tick glyph). */
34
+ export const RAIL_WIDTH = 2;
35
+ /** Terminals narrower than this hide the rail (transcript needs the cols). */
36
+ export const RAIL_MIN_TERMINAL_WIDTH = 60;
37
+ /** Fewer turns than this and the rail is noise. */
38
+ export const RAIL_MIN_TURNS = 2;
39
+ /** Stored preview cap (chars). Render paths re-truncate to card width. */
40
+ export const PREVIEW_MAX_CHARS = 120;
41
+ /** Single eligibility policy: setting, terminal width, turn count, and
42
+ * geometric feasibility in one place. */
43
+ export function railEligible(opts) {
44
+ return (opts.scrollable &&
45
+ opts.turnCount >= RAIL_MIN_TURNS &&
46
+ opts.terminalWidth >= RAIL_MIN_TERMINAL_WIDTH &&
47
+ opts.viewportRows >= 3);
48
+ }
49
+ /**
50
+ * Compute rail geometry, or null when the rail must not render.
51
+ *
52
+ * Windowing: when turns outnumber tick rows (viewport − 2 chevrons), slide
53
+ * a window around the ACTIVE turn; at the bottom prefer the tail so the
54
+ * newest ticks stay visible — but never exclude the active turn, or no
55
+ * tick would highlight. The chevron+tick block is vertically centered in
56
+ * the rail column.
57
+ */
58
+ export function computeRailGeometry(turnCount, viewportRows, activeIndex, atBottom) {
59
+ if (turnCount < RAIL_MIN_TURNS)
60
+ return null;
61
+ const maxTicks = viewportRows - 2;
62
+ if (maxTicks < 1)
63
+ return null;
64
+ let start = 0;
65
+ if (turnCount > maxTicks) {
66
+ const tailStart = turnCount - maxTicks;
67
+ const anchor = activeIndex ?? turnCount - 1;
68
+ start = atBottom
69
+ ? Math.min(anchor, tailStart)
70
+ : Math.min(Math.max(0, anchor - Math.floor(maxTicks / 2)), tailStart);
71
+ }
72
+ const shown = Math.min(turnCount, maxTicks);
73
+ // Vertically center the ▲ + ticks + ▼ stack, like the web rail.
74
+ const blockTop = Math.max(0, Math.floor((viewportRows - (shown + 2)) / 2));
75
+ return {
76
+ windowStart: start,
77
+ windowEnd: start + shown,
78
+ upRow: blockTop,
79
+ tickTop: blockTop + 1,
80
+ downRow: blockTop + 1 + shown,
81
+ };
82
+ }
83
+ /**
84
+ * Which rail interaction a block-local row lands on. The whole rail width
85
+ * is the hit target (no pixel-hunting the glyph).
86
+ */
87
+ export function railHit(geo, row) {
88
+ if (row === geo.upRow)
89
+ return { kind: 'up' };
90
+ if (row === geo.downRow)
91
+ return { kind: 'down' };
92
+ if (row >= geo.tickTop) {
93
+ const rel = row - geo.tickTop;
94
+ if (rel < geo.windowEnd - geo.windowStart) {
95
+ return { kind: 'tick', index: geo.windowStart + rel };
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ /**
101
+ * First non-empty line, char-capped with a `…` marker. Bounded work: the
102
+ * scan stops at the first non-empty line and slices at the cap, so a huge
103
+ * one-line prompt costs O(cap), not O(line length). Char cap (not display
104
+ * width) on purpose — this bounds the stored snapshot; the hover card
105
+ * re-wraps to its own width (wrapPreviewLines).
106
+ */
107
+ export function clipPreview(text, maxChars = PREVIEW_MAX_CHARS) {
108
+ let line = '';
109
+ let rest = text;
110
+ while (rest.length > 0) {
111
+ const nl = rest.indexOf('\n');
112
+ const head = nl === -1 ? rest : rest.slice(0, nl);
113
+ rest = nl === -1 ? '' : rest.slice(nl + 1);
114
+ const trimmed = head.trim();
115
+ if (trimmed.length > 0) {
116
+ line = trimmed;
117
+ break;
118
+ }
119
+ }
120
+ if (line.length <= maxChars)
121
+ return line;
122
+ // Keep the cap INCLUDING the ellipsis marker, like grok's preview. Slice
123
+ // by CODE POINTS, not UTF-16 units — a surrogate pair split mid-pair
124
+ // would emit a lone surrogate to the terminal.
125
+ return `${[...line].slice(0, maxChars - 1).join('')}…`;
126
+ }
127
+ /**
128
+ * Wrap a preview to at most 2 lines by DISPLAY width (CJK-aware — a
129
+ * terminal cell column budget, not a char count). The last line is
130
+ * ellipsized when content remains. Char-by-char accumulation keeps
131
+ * surrogate pairs and (best-effort) combining sequences intact by never
132
+ * splitting below a low surrogate.
133
+ */
134
+ export function wrapPreviewLines(preview, maxWidth) {
135
+ const widthOf = stringWidth;
136
+ const lines = [];
137
+ let current = '';
138
+ let currentW = 0;
139
+ for (const ch of preview) {
140
+ const w = widthOf(ch);
141
+ if (w <= 0) {
142
+ // Zero-width (combining marks, ZWJ): attach for free.
143
+ current += ch;
144
+ continue;
145
+ }
146
+ if (currentW + w > maxWidth) {
147
+ if (lines.length === 1) {
148
+ // Second line full → ellipsize in place and stop.
149
+ return [lines[0], ellipsize(current, maxWidth)];
150
+ }
151
+ lines.push(current);
152
+ current = ch;
153
+ currentW = w;
154
+ continue;
155
+ }
156
+ current += ch;
157
+ currentW += w;
158
+ }
159
+ if (current.length > 0 || lines.length === 0)
160
+ lines.push(current);
161
+ if (lines.length > 2) {
162
+ // Cannot happen (we stop after the second), kept for exhaustiveness.
163
+ return [lines[0], ellipsize(lines[1], maxWidth)];
164
+ }
165
+ return lines.slice(0, 2);
166
+ }
167
+ function ellipsize(line, maxWidth) {
168
+ // Make room for '…' (1 col) then hard-cut by display width.
169
+ let out = '';
170
+ let w = 0;
171
+ for (const ch of line) {
172
+ const cw = stringWidth(ch);
173
+ if (cw > 0 && w + cw > maxWidth - 1)
174
+ break;
175
+ out += ch;
176
+ w += cw;
177
+ }
178
+ return `${out}…`;
179
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"wrap-text.d.ts","sourceRoot":"","sources":["../../../src/ink/wrap-text.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAqCzC;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,GAC3B,MAAM,CA8BR"}
1
+ {"version":3,"file":"wrap-text.d.ts","sourceRoot":"","sources":["../../../src/ink/wrap-text.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAoFzC;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC9B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,GAC3B,MAAM,CAoCR"}
@@ -2,6 +2,48 @@ import sliceAnsi from '../utils/sliceAnsi.js';
2
2
  import { stringWidth } from './stringWidth.js';
3
3
  import { wrapAnsi } from './wrapAnsi.js';
4
4
  const ELLIPSIS = '…';
5
+ // --- cross-mount wrap cache ------------------------------------------------
6
+ // wrapText is pure: (text, maxWidth, wrapType) deterministically maps to one
7
+ // output string. Yet before this cache every consumer re-wrapped from
8
+ // scratch:
9
+ //
10
+ // - dom.ts measureTextNode seeds its per-NODE incremental cache on first
11
+ // measure; virtualization unmounts scrolled-away rows, so scrolling back
12
+ // re-mounts the row's Text nodes and re-runs the FULL wrap (wrap-ansi
13
+ // tokenizes + string-width-measures every word — 100-300ms single-frame
14
+ // yoga spikes when a row of large settled text scrolls in, the dominant
15
+ // long-session scroll stall);
16
+ // - render-node-to-output.ts wraps every VISIBLE text node on every full
17
+ // paint pass (no per-node cache at all there).
18
+ //
19
+ // A content-addressed LRU here makes both paths O(1) for immutable settled
20
+ // text (Markdown memo keeps content identity stable for the whole session).
21
+ // The key embeds the raw text, so V8's content-hash Map lookup matches even
22
+ // a rebuilt string. Budget mirrors line-width-cache: whole-cache clear on
23
+ // overflow (repopulates within a frame or two).
24
+ const WRAP_CACHE_MAX_ENTRIES = 2048;
25
+ const WRAP_CACHE_MAX_CHARS = 2_000_000;
26
+ const wrapCache = new Map();
27
+ let wrapCacheChars = 0;
28
+ /** Values below this are not worth a cache entry (key building costs about
29
+ * as much as the wrap itself for trivial single-word strings). */
30
+ const WRAP_CACHE_MIN_LENGTH = 24;
31
+ function cachedWrap(text, maxWidth, wrapType, compute) {
32
+ if (text.length < WRAP_CACHE_MIN_LENGTH)
33
+ return compute();
34
+ const key = `${maxWidth}\u0000${wrapType}\u0000${text}`;
35
+ const hit = wrapCache.get(key);
36
+ if (hit !== undefined)
37
+ return hit;
38
+ const result = compute();
39
+ if (wrapCache.size >= WRAP_CACHE_MAX_ENTRIES || wrapCacheChars + text.length > WRAP_CACHE_MAX_CHARS) {
40
+ wrapCache.clear();
41
+ wrapCacheChars = 0;
42
+ }
43
+ wrapCache.set(key, result);
44
+ wrapCacheChars += text.length;
45
+ return result;
46
+ }
5
47
  // sliceAnsi may include a boundary-spanning wide char (e.g. CJK at position
6
48
  // end-1 with width 2 overshoots by 1). Retry with a tighter bound once.
7
49
  function sliceFit(text, start, end) {
@@ -36,16 +78,16 @@ function truncate(text, columns, position) {
36
78
  */
37
79
  export default function wrapText(text, maxWidth, wrapType) {
38
80
  if (wrapType === 'wrap') {
39
- return wrapAnsi(text, maxWidth, {
81
+ return cachedWrap(text, maxWidth, wrapType, () => wrapAnsi(text, maxWidth, {
40
82
  trim: false,
41
83
  hard: true,
42
- });
84
+ }));
43
85
  }
44
86
  if (wrapType === 'wrap-trim') {
45
- return wrapAnsi(text, maxWidth, {
87
+ return cachedWrap(text, maxWidth, wrapType, () => wrapAnsi(text, maxWidth, {
46
88
  trim: true,
47
89
  hard: true,
48
- });
90
+ }));
49
91
  }
50
92
  if (wrapType.startsWith('truncate')) {
51
93
  let position = 'end';
@@ -55,7 +97,7 @@ export default function wrapText(text, maxWidth, wrapType) {
55
97
  if (wrapType === 'truncate-start') {
56
98
  position = 'start';
57
99
  }
58
- return truncate(text, maxWidth, position);
100
+ return cachedWrap(text, maxWidth, wrapType, () => truncate(text, maxWidth, position));
59
101
  }
60
102
  return text;
61
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,EAAqB,KAAK,OAAO,EAAsE,MAAM,2BAA2B,CAAA;AAC/I,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAGlE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AA4I3D,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,MAAM,EACN,QAAQ,EACR,UAAkB,EAClB,cAAc,EAAE,kBAAkB,GACnC,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,aAAa,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,oEAAoE;IACpE,eAAe,CAAC,EAAE,cAAc,CAAA;IAChC,iDAAiD;IACjD,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,4BAisEA"}
1
+ {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,EAAqB,KAAK,OAAO,EAAsE,MAAM,2BAA2B,CAAA;AAC/I,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAGlE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AA8J3D,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,MAAM,EACN,QAAQ,EACR,UAAkB,EAClB,cAAc,EAAE,kBAAkB,GACnC,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,aAAa,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,oEAAoE;IACpE,eAAe,CAAC,EAAE,cAAc,CAAA;IAChC,iDAAiD;IACjD,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,4BAusFA"}