@deepseek-harness-tui/dsh-tui 0.8.8 → 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 (215) hide show
  1. package/README.md +27 -8
  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 +24 -7
  21. package/lib/types/components/FileSuggestions.d.ts.map +1 -1
  22. package/lib/types/components/FileSuggestions.js +39 -12
  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/Markdown.d.ts.map +1 -1
  38. package/lib/types/components/Markdown.js +12 -3
  39. package/lib/types/components/MessageList.d.ts +43 -2
  40. package/lib/types/components/MessageList.d.ts.map +1 -1
  41. package/lib/types/components/MessageList.js +553 -41
  42. package/lib/types/components/ModelPicker.d.ts +4 -1
  43. package/lib/types/components/ModelPicker.d.ts.map +1 -1
  44. package/lib/types/components/ModelPicker.js +2 -2
  45. package/lib/types/components/PermissionsPicker.d.ts +21 -0
  46. package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
  47. package/lib/types/components/PermissionsPicker.js +27 -0
  48. package/lib/types/components/PlanPicker.d.ts +15 -0
  49. package/lib/types/components/PlanPicker.d.ts.map +1 -0
  50. package/lib/types/components/PlanPicker.js +18 -0
  51. package/lib/types/components/PresetPicker.d.ts +4 -1
  52. package/lib/types/components/PresetPicker.d.ts.map +1 -1
  53. package/lib/types/components/PresetPicker.js +2 -2
  54. package/lib/types/components/PromptInput.d.ts +4 -3
  55. package/lib/types/components/PromptInput.d.ts.map +1 -1
  56. package/lib/types/components/PromptInput.js +248 -29
  57. package/lib/types/components/RewindPicker.d.ts +12 -1
  58. package/lib/types/components/RewindPicker.d.ts.map +1 -1
  59. package/lib/types/components/RewindPicker.js +6 -4
  60. package/lib/types/components/ScrollbarGutter.d.ts +28 -0
  61. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
  62. package/lib/types/components/ScrollbarGutter.js +68 -0
  63. package/lib/types/components/Select.d.ts +8 -1
  64. package/lib/types/components/Select.d.ts.map +1 -1
  65. package/lib/types/components/Select.js +4 -2
  66. package/lib/types/components/SkillsPicker.d.ts +4 -1
  67. package/lib/types/components/SkillsPicker.d.ts.map +1 -1
  68. package/lib/types/components/SkillsPicker.js +2 -2
  69. package/lib/types/components/SubagentCard.d.ts +2 -1
  70. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  71. package/lib/types/components/SubagentCard.js +6 -1
  72. package/lib/types/components/SubagentDashboard.d.ts +4 -0
  73. package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
  74. package/lib/types/components/SubagentDashboard.js +9 -1
  75. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
  76. package/lib/types/components/SubagentDetailScene.js +5 -4
  77. package/lib/types/components/SuggestionCard.d.ts +63 -0
  78. package/lib/types/components/SuggestionCard.d.ts.map +1 -0
  79. package/lib/types/components/SuggestionCard.js +77 -0
  80. package/lib/types/components/ThemePicker.d.ts +4 -1
  81. package/lib/types/components/ThemePicker.d.ts.map +1 -1
  82. package/lib/types/components/ThemePicker.js +2 -2
  83. package/lib/types/components/ThinkingToggle.d.ts +4 -1
  84. package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
  85. package/lib/types/components/ThinkingToggle.js +2 -2
  86. package/lib/types/components/TimelineRail.d.ts +53 -0
  87. package/lib/types/components/TimelineRail.d.ts.map +1 -0
  88. package/lib/types/components/TimelineRail.js +190 -0
  89. package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
  90. package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
  91. package/lib/types/components/WorkspaceFlowPicker.js +4 -2
  92. package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
  93. package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
  94. package/lib/types/components/WorkspaceMenuPicker.js +19 -0
  95. package/lib/types/components/WorkspacePicker.d.ts +4 -1
  96. package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
  97. package/lib/types/components/WorkspacePicker.js +2 -2
  98. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
  99. package/lib/types/components/approvals/ApprovalPanel.js +5 -1
  100. package/lib/types/components/design-system/ListItem.d.ts +8 -1
  101. package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
  102. package/lib/types/components/design-system/ListItem.js +8 -3
  103. package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
  104. package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
  105. package/lib/types/components/messages/AssistantTextMessage.js +6 -2
  106. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
  107. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  108. package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
  109. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
  110. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  111. package/lib/types/components/messages/AssistantToolUseMessage.js +4 -2
  112. package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
  113. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  114. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  115. package/lib/types/components/questions/AskUserQuestionPanel.js +34 -2
  116. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  117. package/lib/types/components/questions/PlanReviewPanel.js +13 -2
  118. package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
  119. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  120. package/lib/types/components/sessions/SessionListRow.js +4 -2
  121. package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
  122. package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
  123. package/lib/types/components/trajectory/HotspotView.js +8 -2
  124. package/lib/types/components/trajectory/Ledger.d.ts +9 -1
  125. package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
  126. package/lib/types/components/trajectory/Ledger.js +23 -5
  127. package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
  128. package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
  129. package/lib/types/components/trajectory/WaveBand.js +7 -2
  130. package/lib/types/dsh-adapter/channel.d.ts +16 -5
  131. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  132. package/lib/types/dsh-adapter/channel.js +332 -29
  133. package/lib/types/dsh-adapter/index.d.ts +10 -2
  134. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  135. package/lib/types/dsh-adapter/index.js +4 -1
  136. package/lib/types/dsh-adapter/plugin.d.ts +0 -9
  137. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  138. package/lib/types/dsh-adapter/plugin.js +136 -6
  139. package/lib/types/i18n.d.ts +176 -15
  140. package/lib/types/i18n.d.ts.map +1 -1
  141. package/lib/types/i18n.js +51 -8
  142. package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
  143. package/lib/types/ink/components/AlternateScreen.js +13 -1
  144. package/lib/types/ink/components/App.d.ts +12 -1
  145. package/lib/types/ink/components/App.d.ts.map +1 -1
  146. package/lib/types/ink/components/App.js +101 -4
  147. package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
  148. package/lib/types/ink/components/ClockContext.js +2 -0
  149. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  150. package/lib/types/ink/components/ScrollBox.js +170 -91
  151. package/lib/types/ink/constants.d.ts +9 -0
  152. package/lib/types/ink/constants.d.ts.map +1 -1
  153. package/lib/types/ink/constants.js +9 -0
  154. package/lib/types/ink/events/click-event.d.ts +7 -16
  155. package/lib/types/ink/events/click-event.d.ts.map +1 -1
  156. package/lib/types/ink/events/click-event.js +16 -19
  157. package/lib/types/ink/events/event-handlers.d.ts +11 -1
  158. package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
  159. package/lib/types/ink/events/event-handlers.js +2 -0
  160. package/lib/types/ink/events/input-event.d.ts +9 -0
  161. package/lib/types/ink/events/input-event.d.ts.map +1 -1
  162. package/lib/types/ink/events/input-event.js +4 -0
  163. package/lib/types/ink/events/pointer-event.d.ts +61 -0
  164. package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
  165. package/lib/types/ink/events/pointer-event.js +61 -0
  166. package/lib/types/ink/events/wheel-event.d.ts +17 -0
  167. package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
  168. package/lib/types/ink/events/wheel-event.js +20 -0
  169. package/lib/types/ink/geometry-trace.d.ts +51 -0
  170. package/lib/types/ink/geometry-trace.d.ts.map +1 -0
  171. package/lib/types/ink/geometry-trace.js +83 -0
  172. package/lib/types/ink/hit-test.d.ts +56 -1
  173. package/lib/types/ink/hit-test.d.ts.map +1 -1
  174. package/lib/types/ink/hit-test.js +146 -8
  175. package/lib/types/ink/ink.d.ts +58 -1
  176. package/lib/types/ink/ink.d.ts.map +1 -1
  177. package/lib/types/ink/ink.js +230 -29
  178. package/lib/types/ink/node-cache.d.ts +5 -0
  179. package/lib/types/ink/node-cache.d.ts.map +1 -1
  180. package/lib/types/ink/parse-keypress.d.ts +8 -0
  181. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  182. package/lib/types/ink/parse-keypress.js +101 -12
  183. package/lib/types/ink/render-node-to-output.d.ts +10 -0
  184. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  185. package/lib/types/ink/render-node-to-output.js +111 -18
  186. package/lib/types/ink/timeline-rail.d.ts +138 -0
  187. package/lib/types/ink/timeline-rail.d.ts.map +1 -0
  188. package/lib/types/ink/timeline-rail.js +179 -0
  189. package/lib/types/ink/wrap-text.d.ts.map +1 -1
  190. package/lib/types/ink/wrap-text.js +47 -5
  191. package/lib/types/screens/Chat.d.ts.map +1 -1
  192. package/lib/types/screens/Chat.js +819 -337
  193. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  194. package/lib/types/screens/SessionBrowser.js +56 -29
  195. package/lib/types/screens/Settings.d.ts.map +1 -1
  196. package/lib/types/screens/Settings.js +64 -22
  197. package/lib/types/screens/StatusLine.d.ts +2 -1
  198. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  199. package/lib/types/screens/StatusLine.js +17 -1
  200. package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
  201. package/lib/types/screens/TrajectoryScene.js +81 -14
  202. package/lib/types/screens/chatOverlay.d.ts +221 -0
  203. package/lib/types/screens/chatOverlay.d.ts.map +1 -0
  204. package/lib/types/screens/chatOverlay.js +114 -0
  205. package/lib/types/tips.d.ts.map +1 -1
  206. package/lib/types/tips.js +42 -6
  207. package/lib/types/tuiDisplayPrefs.d.ts +15 -0
  208. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  209. package/lib/types/tuiDisplayPrefs.js +9 -0
  210. package/lib/types/utils/debug.d.ts +10 -0
  211. package/lib/types/utils/debug.d.ts.map +1 -1
  212. package/lib/types/utils/debug.js +24 -0
  213. package/lib/types/utils/paths.d.ts.map +1 -1
  214. package/lib/types/utils/paths.js +1 -0
  215. package/package.json +4 -2
@@ -1 +1 @@
1
- {"version":3,"file":"SessionBrowser.d.ts","sourceRoot":"","sources":["../../../src/screens/SessionBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAsBzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAoDxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,WAAW,EACX,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CA4elB"}
1
+ {"version":3,"file":"SessionBrowser.d.ts","sourceRoot":"","sources":["../../../src/screens/SessionBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAsBzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAoDxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,WAAW,EACX,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CAkhBlB"}
@@ -103,6 +103,8 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
103
103
  const topRef = React.useRef(0);
104
104
  const [mode, setMode] = React.useState('list');
105
105
  const [renameText, setRenameText] = React.useState('');
106
+ /** Hover state for the confirm rows (only one is ever mounted). */
107
+ const [confirmHovered, setConfirmHovered] = React.useState(false);
106
108
  // The browser owns the whole screen, so it has to carry its own messages.
107
109
  // `channel.notify` renders into the conversation — which is exactly what is
108
110
  // NOT on screen right now — so a failed delete, a refused resume, or an
@@ -165,6 +167,38 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
165
167
  return byId >= 0 ? byId : Math.max(0, seekSelectable(view.rows, 0, 1));
166
168
  }, [view.rows, focusId]);
167
169
  const focused = sessionAt(view.rows, focus);
170
+ /**
171
+ * Resume a session (Enter and row-click share this path). The screen closes
172
+ * only once the resume actually happened. Closing first and letting a
173
+ * refusal fall through to a notification would send that explanation to the
174
+ * conversation the user is not looking at, and leave them staring at an
175
+ * unchanged transcript wondering what Enter did. `resumeTo` reports its own
176
+ * reasons; this only has to stay put.
177
+ */
178
+ const resumeSession = (target) => {
179
+ runAction(async () => {
180
+ try {
181
+ const result = await channel.resumeTo(target.id);
182
+ if (result.ok) {
183
+ channel.notify(t('resume-resumed'));
184
+ onClose();
185
+ }
186
+ else {
187
+ if (result.reason === 'cancelled')
188
+ return;
189
+ const text = result.reason === 'working'
190
+ ? t('resume-while-working')
191
+ : result.reason === 'unavailable'
192
+ ? t('resume-unavailable')
193
+ : t('session-resume-failed', { err: result.error });
194
+ setNotice({ text, tone: 'error' });
195
+ }
196
+ }
197
+ catch (error) {
198
+ setNotice({ text: t('session-resume-failed', { err: message(error) }), tone: 'error' });
199
+ }
200
+ });
201
+ };
168
202
  // Where the cursor is RIGHT NOW, including moves made earlier in this same
169
203
  // tick. A held arrow key (or a paste) delivers several key events from one
170
204
  // stdin chunk, and every one of them runs before React re-renders — so a
@@ -338,6 +372,13 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
338
372
  else if (key.downArrow) {
339
373
  step(1);
340
374
  }
375
+ else if (key.wheelUp) {
376
+ // 滚轮在列表上:与 ↑ 同路径——焦点跟随窗口下移焦点即滚动
377
+ step(-1);
378
+ }
379
+ else if (key.wheelDown) {
380
+ step(1);
381
+ }
341
382
  else if (key.pageUp || key.pageDown) {
342
383
  // A page is "as many rows as the window holds", taken as repeated single
343
384
  // steps so it lands on a selectable row like every other move.
@@ -346,34 +387,7 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
346
387
  else if (isPlainReturn(key)) {
347
388
  if (focused === undefined)
348
389
  return;
349
- const target = focused;
350
- // The screen closes only once the resume actually happened. Closing
351
- // first and letting a refusal fall through to a notification would send
352
- // that explanation to the conversation the user is not looking at, and
353
- // leave them staring at an unchanged transcript wondering what Enter
354
- // did. `resumeTo` reports its own reasons; this only has to stay put.
355
- runAction(async () => {
356
- try {
357
- const result = await channel.resumeTo(target.id);
358
- if (result.ok) {
359
- channel.notify(t('resume-resumed'));
360
- onClose();
361
- }
362
- else {
363
- if (result.reason === 'cancelled')
364
- return;
365
- const text = result.reason === 'working'
366
- ? t('resume-while-working')
367
- : result.reason === 'unavailable'
368
- ? t('resume-unavailable')
369
- : t('session-resume-failed', { err: result.error });
370
- setNotice({ text, tone: 'error' });
371
- }
372
- }
373
- catch (error) {
374
- setNotice({ text: t('session-resume-failed', { err: message(error) }), tone: 'error' });
375
- }
376
- });
390
+ resumeSession(focused);
377
391
  }
378
392
  else if (key.escape) {
379
393
  // Esc backs out one layer at a time: a live query first, the screen
@@ -455,5 +469,18 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
455
469
  t('session-hint-list-mid', { mod: modLabel }),
456
470
  t('session-hint-list-short'),
457
471
  ], inputBudget);
458
- return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "remember", bold: true, children: header.left }), _jsx(Text, { dimColor: true, children: `${' '.repeat(header.gap)}${header.right}` })] }), rules.has(0) && (_jsx(Box, { flexShrink: 0, children: _jsx(Divider, { width: columns }) })), _jsx(Box, { flexShrink: 0, children: _jsx(SearchBox, { query: tailWidth(filters.query, inputBudget), isFocused: mode === 'list', isTerminalFocused: isTerminalFocused, placeholder: truncateWidth(t('session-search-placeholder', { scope }), inputBudget), borderless: true }) }), rules.has(1) && (_jsx(Box, { flexShrink: 0, children: _jsx(Divider, { width: columns }) })), _jsxs(Box, { flexGrow: 1, flexShrink: 1, children: [!soloPreview && (_jsxs(Box, { flexDirection: "column", width: listWidth, height: listHeight, flexShrink: 0, children: [!loaded && (_jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(t('session-loading'), listWidth - 2)}` })), loaded && view.rows.length === 0 && (_jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(t('resume-none-in-cwd'), listWidth - 2)}` })), visible.map((row, index) => row.kind === 'project' ? (_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "planMode", children: truncateWidth(` ${formatProject(row.project, home)}`, listWidth - 6) }), _jsx(Text, { dimColor: true, children: ` ${row.count}` })] }, `project:${row.project}:${index}`)) : (_jsx(SessionListRow, { session: row.session, width: listWidth, depth: row.depth, focused: windowTop + index === focus, now: now }, row.session.id)))] })), (splitPreview || soloPreview) && focused !== undefined && (_jsx(SessionPreview, { session: focused, entries: preview, loading: previewLoading, width: previewWidth, height: listHeight, home: home, now: now }))] }), notice !== undefined && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: notice.tone === 'error' ? 'error' : 'success', children: ` ${truncateWidth(notice.text, Math.max(0, columns - 2))}` }) })), mode === 'confirm-delete' && focused !== undefined && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: "error", children: ` ${truncateWidth(t('resume-delete-confirm', { name: focused.title.text }), inputBudget)}` }) })), mode === 'confirm-clean' && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: "warning", children: ` ${truncateWidth(t('session-clean-confirm', { n: view.emptyCount }), inputBudget)}` }) })), mode === 'rename' && (_jsx(Box, { flexShrink: 0, children: _jsx(SearchBox, { query: tailWidth(renameText, inputBudget), isFocused: true, isTerminalFocused: isTerminalFocused, placeholder: truncateWidth(t('resume-rename-placeholder'), inputBudget), prefix: "\u270E", borderless: true }) })), rules.has(2) && (_jsx(Box, { flexShrink: 0, children: _jsx(Divider, { width: columns }) })), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: hint }) }) })] }));
472
+ return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "remember", bold: true, children: header.left }), _jsx(Text, { dimColor: true, children: `${' '.repeat(header.gap)}${header.right}` })] }), rules.has(0) && (_jsx(Box, { flexShrink: 0, children: _jsx(Divider, { width: columns }) })), _jsx(Box, { flexShrink: 0, children: _jsx(SearchBox, { query: tailWidth(filters.query, inputBudget), isFocused: mode === 'list', isTerminalFocused: isTerminalFocused, placeholder: truncateWidth(t('session-search-placeholder', { scope }), inputBudget), borderless: true }) }), rules.has(1) && (_jsx(Box, { flexShrink: 0, children: _jsx(Divider, { width: columns }) })), _jsxs(Box, { flexGrow: 1, flexShrink: 1, children: [!soloPreview && (_jsxs(Box, { flexDirection: "column", width: listWidth, height: listHeight, flexShrink: 0, children: [!loaded && (_jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(t('session-loading'), listWidth - 2)}` })), loaded && view.rows.length === 0 && (_jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(t('resume-none-in-cwd'), listWidth - 2)}` })), visible.map((row, index) => row.kind === 'project' ? (_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "planMode", children: truncateWidth(` ${formatProject(row.project, home)}`, listWidth - 6) }), _jsx(Text, { dimColor: true, children: ` ${row.count}` })] }, `project:${row.project}:${index}`)) : (_jsx(SessionListRow, { session: row.session, width: listWidth, depth: row.depth, focused: windowTop + index === focus, now: now,
473
+ // 点击行 = 聚焦 + 恢复该会话(与 Enter 同路径)
474
+ onClick: () => {
475
+ setFocusId(row.session.id);
476
+ resumeSession(row.session);
477
+ } }, row.session.id)))] })), (splitPreview || soloPreview) && focused !== undefined && (_jsx(SessionPreview, { session: focused, entries: preview, loading: previewLoading, width: previewWidth, height: listHeight, home: home, now: now }))] }), notice !== undefined && (_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: notice.tone === 'error' ? 'error' : 'success', children: ` ${truncateWidth(notice.text, Math.max(0, columns - 2))}` }) })), mode === 'confirm-delete' && focused !== undefined && (_jsx(Box, { flexShrink: 0, onClick: () => {
478
+ // 点击确认行 = 确认删除(与 Enter 同路径);确认屏本身就是
479
+ // 显式确认层,取消保留键盘 Esc,防误点
480
+ setMode('list');
481
+ runDelete(focused);
482
+ }, onMouseEnter: () => setConfirmHovered(true), onMouseLeave: () => setConfirmHovered(false), backgroundColor: confirmHovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Text, { color: "error", children: ` ${truncateWidth(t('resume-delete-confirm', { name: focused.title.text }), inputBudget)}` }) })), mode === 'confirm-clean' && (_jsx(Box, { flexShrink: 0, onClick: () => {
483
+ setMode('list');
484
+ runClean();
485
+ }, onMouseEnter: () => setConfirmHovered(true), onMouseLeave: () => setConfirmHovered(false), backgroundColor: confirmHovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Text, { color: "warning", children: ` ${truncateWidth(t('session-clean-confirm', { n: view.emptyCount }), inputBudget)}` }) })), mode === 'rename' && (_jsx(Box, { flexShrink: 0, children: _jsx(SearchBox, { query: tailWidth(renameText, inputBudget), isFocused: true, isTerminalFocused: isTerminalFocused, placeholder: truncateWidth(t('resume-rename-placeholder'), inputBudget), prefix: "\u270E", borderless: true }) })), rules.has(2) && (_jsx(Box, { flexShrink: 0, children: _jsx(Divider, { width: columns }) })), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: hint }) }) })] }));
459
486
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../../src/screens/Settings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AA+CxD;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EACP,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CA0ZlB"}
1
+ {"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../../src/screens/Settings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAWzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AA+CxD;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EACP,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CAkdlB"}
@@ -160,6 +160,39 @@ export function Settings({ channel, onClose, }) {
160
160
  setNotice(undefined);
161
161
  bump();
162
162
  };
163
+ /**
164
+ * Activate one focusable entry — the keyboard Enter path, shared with the
165
+ * mouse click. Groups open their subpage; boolean/select fields cycle their
166
+ * value; text/secret fields enter the edit mode.
167
+ */
168
+ const activateEntry = (entry) => {
169
+ if (entry.kind === 'group') {
170
+ setActiveGroup({ ns: entry.ns, id: entry.group.id });
171
+ setFocusIndex(0);
172
+ setWindowStart(0);
173
+ return;
174
+ }
175
+ // A save in flight owns the section's drafts; starting an edit mid-write
176
+ // is exactly the lost-draft race the staged model exists to prevent.
177
+ const form = forms.get(entry.ns);
178
+ if (form === undefined || !form.available || form.saving)
179
+ return;
180
+ if (entry.field.kind === 'boolean' || entry.field.kind === 'select') {
181
+ cycleField(entry.ns, entry.field);
182
+ }
183
+ else {
184
+ setEditing({ ns: entry.ns, field: entry.field, draft: form.field(entry.field).text });
185
+ setMode('edit');
186
+ }
187
+ };
188
+ /** Mouse wheel moves the focus (the window is focus-follow, so moving the
189
+ * focus IS scrolling); the edit mode keeps the keyboard as sole owner. */
190
+ const handleWheel = (event) => {
191
+ if (mode === 'edit')
192
+ return;
193
+ const direction = event.deltaY >= 0 ? 1 : -1;
194
+ setFocusIndex(previous => Math.min(Math.max(0, focusable.length - 1), Math.max(0, previous + direction)));
195
+ };
163
196
  useInput((input, key) => {
164
197
  if (mode === 'edit' && editing !== null) {
165
198
  if (isPlainReturn(key)) {
@@ -192,23 +225,7 @@ export function Settings({ channel, onClose, }) {
192
225
  setFocusIndex(Math.min(Math.max(0, focusable.length - 1), effFocus + 1));
193
226
  }
194
227
  else if (isPlainReturn(key) && focused !== undefined) {
195
- if (focused.kind === 'group') {
196
- setActiveGroup({ ns: focused.ns, id: focused.group.id });
197
- setFocusIndex(0);
198
- setWindowStart(0);
199
- return;
200
- }
201
- // A save in flight owns the section's drafts; starting an edit mid-write
202
- // is exactly the lost-draft race the staged model exists to prevent.
203
- if (focusedForm === undefined || !focusedForm.available || focusedForm.saving)
204
- return;
205
- if (focused.field.kind === 'boolean' || focused.field.kind === 'select') {
206
- cycleField(focused.ns, focused.field);
207
- }
208
- else {
209
- setEditing({ ns: focused.ns, field: focused.field, draft: focusedForm.field(focused.field).text });
210
- setMode('edit');
211
- }
228
+ activateEntry(focused);
212
229
  }
213
230
  else if (input === 's' && focused !== undefined) {
214
231
  commitSave(focused.ns);
@@ -246,7 +263,7 @@ export function Settings({ channel, onClose, }) {
246
263
  }
247
264
  }
248
265
  });
249
- const renderField = (section, field) => {
266
+ const renderField = (section, field, focus) => {
250
267
  const ns = section.ns;
251
268
  const form = forms.get(ns);
252
269
  const state = form?.field(field) ?? { text: '', overridden: false, invalid: false };
@@ -254,6 +271,20 @@ export function Settings({ channel, onClose, }) {
254
271
  const isEditing = isFocused && mode === 'edit' && editing !== null;
255
272
  const label = pick(field.label, field.descriptions);
256
273
  const hint = field.hint !== undefined ? pick(field.hint, field.hintDescriptions) : undefined;
274
+ // 鼠标:编辑态整屏不响应(草稿由键盘独占,防误触抢焦点);列表态
275
+ // 悬停即移动焦点(lazygit 语义——焦点就是选中),点击 = 焦点 + 该行
276
+ // 的 Enter 动作(boolean/select 循环值,文本/secret 进编辑态)。
277
+ const rowEvents = mode === 'edit'
278
+ ? undefined
279
+ : {
280
+ onClick: () => {
281
+ setFocusIndex(focus);
282
+ activateEntry({ kind: 'field', ns: section.ns, field });
283
+ },
284
+ onMouseEnter: () => {
285
+ setFocusIndex(focus);
286
+ },
287
+ };
257
288
  let value;
258
289
  if (field.secret !== undefined) {
259
290
  const configured = secrets.get(`${ns}:${field.path.join('.')}`) === true;
@@ -276,7 +307,7 @@ export function Settings({ channel, onClose, }) {
276
307
  else {
277
308
  value = state.text;
278
309
  }
279
- return (_jsxs(Box, { children: [_jsx(Text, { color: isFocused ? 'suggestion' : undefined, children: isFocused ? '❯ ' : ' ' }), _jsx(Text, { bold: isFocused, children: label }), _jsx(Box, { flexGrow: 1 }), state.invalid && _jsxs(Text, { color: "error", children: [t('settings-field-invalid'), " "] }), state.overridden && _jsxs(Text, { dimColor: true, children: ["[", t('settings-badge-override'), "] "] }), form?.isStaged(field) === true && !isEditing && _jsx(Text, { color: "suggestion", children: "* " }), _jsx(Text, { color: isEditing || isFocused ? 'suggestion' : undefined, dimColor: !isFocused && !isEditing && state.text === '', children: value }), hint !== undefined && isFocused && (_jsxs(Text, { dimColor: true, children: ['\n ', hint] }))] }));
310
+ return (_jsxs(Box, { ...rowEvents, children: [_jsx(Text, { color: isFocused ? 'suggestion' : undefined, children: isFocused ? '❯ ' : ' ' }), _jsx(Text, { bold: isFocused, children: label }), _jsx(Box, { flexGrow: 1 }), state.invalid && _jsxs(Text, { color: "error", children: [t('settings-field-invalid'), " "] }), state.overridden && _jsxs(Text, { dimColor: true, children: ["[", t('settings-badge-override'), "] "] }), form?.isStaged(field) === true && !isEditing && _jsx(Text, { color: "suggestion", children: "* " }), _jsx(Text, { color: isEditing || isFocused ? 'suggestion' : undefined, dimColor: !isFocused && !isEditing && state.text === '', children: value }), hint !== undefined && isFocused && (_jsxs(Text, { dimColor: true, children: ['\n ', hint] }))] }));
280
311
  };
281
312
  // ── Layout: a flat entry list with accounted line heights, windowed so the
282
313
  // focused row is always on screen no matter how long the current page gets. ─
@@ -290,7 +321,7 @@ export function Settings({ channel, onClose, }) {
290
321
  key: `field:${section.ns}:${field.path.join('.')}`,
291
322
  lines: field.hint !== undefined && isFocused ? 2 : 1,
292
323
  focus: index,
293
- node: renderField(section, field),
324
+ node: renderField(section, field, index),
294
325
  });
295
326
  };
296
327
  if (activeSection !== undefined && activeGroupSpec !== undefined) {
@@ -319,11 +350,22 @@ export function Settings({ channel, onClose, }) {
319
350
  const isFocused = focused?.kind === 'group' && focused.ns === section.ns && focused.group === group;
320
351
  const index = focusCursor;
321
352
  focusCursor += 1;
353
+ const groupRowEvents = mode === 'edit'
354
+ ? undefined
355
+ : {
356
+ onClick: () => {
357
+ setFocusIndex(index);
358
+ activateEntry({ kind: 'group', ns: section.ns, group });
359
+ },
360
+ onMouseEnter: () => {
361
+ setFocusIndex(index);
362
+ },
363
+ };
322
364
  entries.push({
323
365
  key: `group:${section.ns}:${group.id}`,
324
366
  lines: 1,
325
367
  focus: index,
326
- node: (_jsxs(Box, { children: [_jsx(Text, { color: isFocused ? 'suggestion' : undefined, children: isFocused ? '❯ ' : ' ' }), _jsx(Text, { bold: isFocused, children: pick(group.title, group.descriptions) }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: isFocused ? 'suggestion' : undefined, children: "\u203A" })] })),
368
+ node: (_jsxs(Box, { ...groupRowEvents, children: [_jsx(Text, { color: isFocused ? 'suggestion' : undefined, children: isFocused ? '❯ ' : ' ' }), _jsx(Text, { bold: isFocused, children: pick(group.title, group.descriptions) }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: isFocused ? 'suggestion' : undefined, children: "\u203A" })] })),
327
369
  });
328
370
  }
329
371
  });
@@ -388,5 +430,5 @@ export function Settings({ channel, onClose, }) {
388
430
  ? `${t('settings-title')} › ${pick(activeSection.title, activeSection.descriptions)} › ${pick(activeGroupSpec.title, activeGroupSpec.descriptions)}`
389
431
  : t('settings-title');
390
432
  const navigationHint = activeGroupSpec === undefined ? t('settings-hint-list') : t('settings-hint-group');
391
- return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: title }), _jsx(Box, { flexGrow: 1 }), host === undefined && _jsx(Text, { color: "warning", children: t('settings-unavailable') })] }), _jsx(Divider, {}), visible.map(entry => (_jsx(React.Fragment, { children: entry.node }, entry.key))), _jsx(Box, { flexGrow: 1 }), notice !== undefined && (_jsx(Text, { color: notice.tone === 'error' ? 'error' : 'success', children: notice.text })), _jsx(Divider, {}), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: mode === 'edit' ? t('settings-hint-edit') : navigationHint }) })] }));
433
+ return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: title }), _jsx(Box, { flexGrow: 1 }), host === undefined && _jsx(Text, { color: "warning", children: t('settings-unavailable') })] }), _jsx(Divider, {}), _jsx("ink-box", { style: { flexDirection: 'column', flexGrow: 1, flexShrink: 1, overflow: 'hidden' }, onWheel: handleWheel, children: visible.map(entry => (_jsx(React.Fragment, { children: entry.node }, entry.key))) }), _jsx(Box, { flexGrow: 1 }), notice !== undefined && (_jsx(Text, { color: notice.tone === 'error' ? 'error' : 'success', children: notice.text })), _jsx(Divider, {}), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: mode === 'edit' ? t('settings-hint-edit') : navigationHint }) })] }));
392
434
  }
@@ -5,7 +5,8 @@ import type { WaveBand } from '../dsh-adapter/types.js';
5
5
  * The footer under the prompt input, in Claude Code's PromptInputFooter
6
6
  * layout: the segmented context progress bar on its own first line, the
7
7
  * status line below (left group: model · tokens · think level · cache · tps
8
- * gauge/sparkline; right group: git · cwd · title, right-aligned), and the
8
+ * gauge/sparkline; right group: git · cwd · title · short session id,
9
+ * right-aligned), and the
9
10
  * mode/hint line last. The right side of the footer shows the latest
10
11
  * transient notification (errors in red, warnings in amber — CC style).
11
12
  */
@@ -1 +1 @@
1
- {"version":3,"file":"StatusLine.d.ts","sourceRoot":"","sources":["../../../src/screens/StatusLine.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAGxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAQvD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,eAAuB,EACvB,QAAgB,EAChB,IAAI,GACL,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACvD,qBA0PA;AAED,KAAK,aAAa,GAAG;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKvF"}
1
+ {"version":3,"file":"StatusLine.d.ts","sourceRoot":"","sources":["../../../src/screens/StatusLine.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAQzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAGxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAQvD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,eAAuB,EACvB,QAAgB,EAChB,IAAI,GACL,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACvD,qBA8QA;AAED,KAAK,aAAa,GAAG;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKvF"}
@@ -5,6 +5,7 @@ import { t } from '../i18n.js';
5
5
  import { formatContextUsage, DEFAULT_STATUS_BAR, normalizeStatusBar } from '../tuiDisplayPrefs.js';
6
6
  import { Byline } from '../components/design-system/Byline.js';
7
7
  import { ActivityLine, contextPressurePct } from '../components/ActivityLine.js';
8
+ import { GoalStatusChip } from '../components/GoalTodoPanel.js';
8
9
  import { modeDisplayName } from '../sessionModes.js';
9
10
  import { MiniWake } from '../components/trajectory/MiniWake.js';
10
11
  import { renderContextBar, renderTpsGauge, renderTpsSparkline, speedColor, } from './StatusMetrics.js';
@@ -12,7 +13,8 @@ import { renderContextBar, renderTpsGauge, renderTpsSparkline, speedColor, } fro
12
13
  * The footer under the prompt input, in Claude Code's PromptInputFooter
13
14
  * layout: the segmented context progress bar on its own first line, the
14
15
  * status line below (left group: model · tokens · think level · cache · tps
15
- * gauge/sparkline; right group: git · cwd · title, right-aligned), and the
16
+ * gauge/sparkline; right group: git · cwd · title · short session id,
17
+ * right-aligned), and the
16
18
  * mode/hint line last. The right side of the footer shows the latest
17
19
  * transient notification (errors in red, warnings in amber — CC style).
18
20
  */
@@ -75,6 +77,12 @@ export function StatusLine({ channel, selectionActive = false, helpOpen = false,
75
77
  : []),
76
78
  ];
77
79
  const rightParts = [
80
+ // Goal chip first: session-level state outranks repo/location details.
81
+ ...(statusBar.goal && channel.goal !== undefined
82
+ ? [
83
+ _jsx(GoalStatusChip, { goal: channel.goal, minimal: channel.minimal }, "goal"),
84
+ ]
85
+ : []),
78
86
  ...(statusBar.gitBranch && channel.gitBranch
79
87
  ? [
80
88
  _jsx(Text, { color: "professionalBlue", children: channel.gitBranch }, "git"),
@@ -90,6 +98,14 @@ export function StatusLine({ channel, selectionActive = false, helpOpen = false,
90
98
  _jsx(Text, { dimColor: true, children: channel.sessionTitle }, "title"),
91
99
  ]
92
100
  : []),
101
+ // Short id last: a provenance tag trails the content it identifies, and
102
+ // the 8-char form is what the session log filename starts with, so a
103
+ // truncated rendering still names the right log for --resume.
104
+ ...(statusBar.sessionId && channel.agentId
105
+ ? [
106
+ _jsx(Text, { dimColor: true, children: `#${channel.agentId.slice(0, 8)}` }, "sessionId"),
107
+ ]
108
+ : []),
93
109
  ];
94
110
  const hint = selectionActive
95
111
  ? t('statusline-hint-select')
@@ -1 +1 @@
1
- {"version":3,"file":"TrajectoryScene.d.ts","sourceRoot":"","sources":["../../../src/screens/TrajectoryScene.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAczB,OAAO,EAML,KAAK,SAAS,EACf,MAAM,oCAAoC,CAAA;AAE3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AA+BxD,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,SAAS,CAAA;AAEnD,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,KAAK,EACL,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB,sDAAsD;IACtD,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CAyYlB"}
1
+ {"version":3,"file":"TrajectoryScene.d.ts","sourceRoot":"","sources":["../../../src/screens/TrajectoryScene.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAezB,OAAO,EAML,KAAK,SAAS,EACf,MAAM,oCAAoC,CAAA;AAE3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AA+BxD,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,SAAS,CAAA;AAEnD,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,KAAK,EACL,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB,sDAAsD;IACtD,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CA2hBlB"}
@@ -64,6 +64,11 @@ export function TrajectoryScene({ channel, build, onClose, }) {
64
64
  const [arrivalFrom, setArrivalFrom] = React.useState(Number.MAX_SAFE_INTEGER);
65
65
  /** Cursor pinned to the tail until the user scrolls away from it. */
66
66
  const [follow, setFollow] = React.useState(true);
67
+ /** Mouse hover states for the tab segments and the sort/projection label. */
68
+ const [hoverTab, setHoverTab] = React.useState(null);
69
+ const [hoverAxis, setHoverAxis] = React.useState(false);
70
+ /** Hover state for the header ✕ exit button. */
71
+ const [closeHovered, setCloseHovered] = React.useState(false);
67
72
  // ── projection ───────────────────────────────────────────────────────────
68
73
  const nodes = build.nodes;
69
74
  const query = React.useMemo(() => parseQuery(queryText), [queryText]);
@@ -147,6 +152,40 @@ export function TrajectoryScene({ channel, build, onClose, }) {
147
152
  setExpanded(false);
148
153
  setInspectScroll(0);
149
154
  }, [tick]);
155
+ /**
156
+ * Jump back to the timeline, positioned on a hotspot group's first member —
157
+ * the keyboard Enter path, shared with the mouse row click.
158
+ */
159
+ const jumpFromHotspot = React.useCallback((row) => {
160
+ switchView('timeline');
161
+ if (row !== undefined) {
162
+ const target = indexes.indexOf(row.firstIndex);
163
+ setCursor(target >= 0 ? target : 0);
164
+ setFollow(false);
165
+ }
166
+ }, [indexes, switchView]);
167
+ /** Mouse wheel over the content area: move the selection, not a viewport. */
168
+ const handleWheel = React.useCallback((event) => {
169
+ if (view === 'hotspot') {
170
+ const total = hotspotRows(agg).length;
171
+ const direction = event.deltaY >= 0 ? 1 : -1;
172
+ setHotCursor(previous => Math.max(0, Math.min(total - 1, previous + direction)));
173
+ return;
174
+ }
175
+ if (expanded) {
176
+ // Expanded inspector owns the wheel: same page step as j/k.
177
+ const direction = event.deltaY >= 0 ? 1 : -1;
178
+ setInspectScroll(previous => Math.max(0, previous + direction * Math.max(1, inspectorRows - 2)));
179
+ return;
180
+ }
181
+ move(event.deltaY);
182
+ }, [view, agg, expanded, inspectorRows, move]);
183
+ /** Jump the timeline cursor to a filtered index (keyboard jump semantics). */
184
+ const jumpTo = React.useCallback((index) => {
185
+ setInspectScroll(0);
186
+ setCursor(index);
187
+ setFollow(index >= filtered.length - 1);
188
+ }, [filtered.length]);
150
189
  // ── keys ─────────────────────────────────────────────────────────────────
151
190
  useInput((input, key) => {
152
191
  // The query line owns the keyboard while open, so a `q` typed into a
@@ -208,14 +247,7 @@ export function TrajectoryScene({ channel, build, onClose, }) {
208
247
  }
209
248
  if (key.return) {
210
249
  // Jump back to the timeline, positioned on the group's first member.
211
- const row = hotspotRows(agg)[hotCursor];
212
- switchView('timeline');
213
- if (row !== undefined) {
214
- const target = indexes.indexOf(row.firstIndex);
215
- setCursor(target >= 0 ? target : 0);
216
- setFollow(false);
217
- }
218
- return;
250
+ return jumpFromHotspot(hotspotRows(agg)[hotCursor]);
219
251
  }
220
252
  return;
221
253
  }
@@ -286,16 +318,42 @@ export function TrajectoryScene({ channel, build, onClose, }) {
286
318
  (totals.errors > 0 ? ` \u00b7 ${t('traj-errors', { n: totals.errors })}` : '') +
287
319
  (totals.retries > 0 ? ` \u00b7 ${t('traj-retries', { n: totals.retries })}` : '') +
288
320
  ` \u00b7 ${formatDuration(totals.spanMs)}`;
289
- const headerLine = spread(`\u2726 ${t('traj-title')} ${channel.sessionTitle ?? channel.cwd}`, totalsText, bandWidth);
290
- const header = (_jsx(Box, { width: "100%", height: 1, flexShrink: 0, children: _jsxs(Text, { children: [_jsx(Text, { color: "claude", bold: true, children: `\u2726 ${t('traj-title')}` }), _jsx(Text, { color: "subtle", children: headerLine.left.slice((`\u2726 ${t('traj-title')}`).length) }), _jsx(Text, { children: headerLine.gap }), _jsx(Text, { color: totals.errors > 0 ? 'error' : 'subtle', children: headerLine.right })] }) }));
321
+ // ✕ 退出按钮占 2 格(` ✕`):预量测行给右端留出预算,按钮钉在末列
322
+ const CLOSE_WIDTH = 2;
323
+ const headerLine = spread(`\u2726 ${t('traj-title')} ${channel.sessionTitle ?? channel.cwd}`, totalsText, bandWidth - CLOSE_WIDTH);
324
+ const header = (_jsxs(Box, { width: "100%", height: 1, flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, width: bandWidth - CLOSE_WIDTH, children: _jsxs(Text, { children: [_jsx(Text, { color: "claude", bold: true, children: `\u2726 ${t('traj-title')}` }), _jsx(Text, { color: "subtle", children: headerLine.left.slice((`\u2726 ${t('traj-title')}`).length) }), _jsx(Text, { children: headerLine.gap }), _jsx(Text, { color: totals.errors > 0 ? 'error' : 'subtle', children: headerLine.right })] }) }), _jsx(Box, { flexShrink: 0, width: CLOSE_WIDTH,
325
+ // 可点击退出(q/Esc 的鼠标等价)——hover 提亮给出可点指示
326
+ onClick: () => onClose(), onMouseEnter: () => setCloseHovered(true), onMouseLeave: () => setCloseHovered(false), children: _jsx(Text, { color: closeHovered ? 'text' : 'subtle', children: ' ✕' }) })] }));
291
327
  const axisLabel = view === 'hotspot' ? t(`traj-sort-${sort}`) : t(`traj-proj-${projection}`);
292
- const tabsLeft = `${view === 'timeline' ? '\u25cf' : '\u25cb'} ${t('traj-tab-timeline')} ` +
293
- `${view === 'hotspot' ? '\u25cf' : '\u25cb'} ${t('traj-tab-hotspot')}`;
328
+ const tabTimelineText = `${view === 'timeline' ? '\u25cf' : '\u25cb'} ${t('traj-tab-timeline')} `;
329
+ const tabHotspotText = `${view === 'hotspot' ? '\u25cf' : '\u25cb'} ${t('traj-tab-hotspot')}`;
294
330
  const queryText_ = queryOpen || !query.empty
295
331
  ? ` / ${queryText}${queryOpen ? '\u258c' : ''} ${t('traj-matches', { n: filtered.length, total: nodes.length })}`
296
332
  : '';
333
+ const tabsLeft = tabTimelineText + tabHotspotText;
297
334
  const tabsLine = spread(tabsLeft + queryText_, axisLabel, bandWidth);
298
- const tabs = (_jsx(Box, { width: "100%", height: 1, flexShrink: 0, children: _jsxs(Text, { children: [_jsx(Text, { color: view === 'timeline' ? 'permission' : 'subtle', bold: view === 'timeline', children: tabsLine.left.slice(0, tabsLeft.length) }), _jsx(Text, { color: "suggestion", children: tabsLine.left.slice(tabsLeft.length) }), _jsx(Text, { children: tabsLine.gap }), _jsx(Text, { color: "subtle", children: tabsLine.right })] }) }));
335
+ // Segment truncation mirrors spread's left-clip: the query tail yields
336
+ // first, then the far tab label — the line stays exactly one row.
337
+ const leftRoom = stringWidth(tabsLine.left);
338
+ const hotspotShown = truncateWidth(tabHotspotText, Math.max(0, leftRoom - stringWidth(tabTimelineText)));
339
+ const queryShown = queryText_ === ''
340
+ ? ''
341
+ : truncateWidth(queryText_, Math.max(0, leftRoom - stringWidth(tabTimelineText) - stringWidth(hotspotShown)));
342
+ const tabs = (_jsxs(Box, { width: "100%", height: 1, flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, width: stringWidth(tabTimelineText), onClick: () => switchView('timeline'), onMouseEnter: () => setHoverTab('timeline'), onMouseLeave: () => setHoverTab(previous => (previous === 'timeline' ? null : previous)), children: _jsx(Text, { color: view === 'timeline' ? 'permission' : hoverTab === 'timeline' ? 'text' : 'subtle', bold: view === 'timeline', children: tabTimelineText }) }), _jsx(Box, { flexShrink: 0, width: stringWidth(hotspotShown), onClick: () => switchView('hotspot'), onMouseEnter: () => setHoverTab('hotspot'), onMouseLeave: () => setHoverTab(previous => (previous === 'hotspot' ? null : previous)), children: _jsx(Text, { color: view === 'hotspot' ? 'permission' : hoverTab === 'hotspot' ? 'text' : 'subtle', bold: view === 'hotspot', children: hotspotShown }) }), queryShown !== '' && (_jsx(Box, { flexShrink: 0, width: stringWidth(queryShown), onClick: () => setQueryOpen(true), children: _jsx(Text, { color: "suggestion", children: queryShown }) })), _jsx(Box, { flexGrow: 1, flexShrink: 1, onClick: () => { if (!queryOpen)
343
+ setQueryOpen(true); }, children: _jsx(Text, { children: tabsLine.gap }) }), _jsx(Box, { flexShrink: 0,
344
+ // +1 slack: ink breaks a wrap line exactly AT the box width, so a
345
+ // wide-char-ending label (按耗时) at an exact fit loses its last
346
+ // glyph to a clipped second line (the truncateWidth doc's trap).
347
+ // The flexGrow gap absorbs the extra cell.
348
+ width: stringWidth(tabsLine.right) + 1, onClick: () => {
349
+ if (view === 'hotspot') {
350
+ setSort(previous => HOTSPOT_SORTS[(HOTSPOT_SORTS.indexOf(previous) + 1) % HOTSPOT_SORTS.length]);
351
+ }
352
+ else {
353
+ setProjection(previous => WAVE_PROJECTIONS[(WAVE_PROJECTIONS.indexOf(previous) + 1) % WAVE_PROJECTIONS.length]);
354
+ }
355
+ setSwitchTick(tick);
356
+ }, onMouseEnter: () => setHoverAxis(true), onMouseLeave: () => setHoverAxis(false), children: _jsx(Text, { color: hoverAxis ? 'text' : 'subtle', children: tabsLine.right }) })] }));
299
357
  const hints = view === 'hotspot'
300
358
  ? t('traj-hint-hotspot')
301
359
  : queryOpen
@@ -303,5 +361,14 @@ export function TrajectoryScene({ channel, build, onClose, }) {
303
361
  : expanded
304
362
  ? t('traj-hint-expanded')
305
363
  : t('traj-hint-timeline');
306
- return (_jsxs(Box, { ref: ref, flexDirection: "column", width: "100%", paddingX: 1, children: [header, tabs, _jsx(WaveBand, { band: band, width: bandWidth, cursorColumn: columnOfIndex(band, indexes[clampedCursor] ?? 0), viewportStart: columnOfIndex(band, indexes[windowStart] ?? 0), viewportEnd: columnOfIndex(band, indexes[Math.min(filtered.length - 1, windowStart + ledgerRows - 1)] ?? 0), matches: matchColumns, tick: tick, alertTick: alertTick }), _jsx(Box, { height: 1, flexShrink: 0, children: _jsx(Text, { children: " " }) }), view === 'timeline' ? (_jsxs(_Fragment, { children: [_jsx(Ledger, { rows: filtered, start: windowStart, height: ledgerRows, cursor: clampedCursor, width: columns - 4, tick: tick, arrivalTick: arrivalTick, arrivalFrom: arrivalFrom }), _jsx(Divider, { color: "permission", width: bandWidth }), _jsx(Inspector, { node: focused, detail: detail, height: inspectorRows, width: columns - 4, expanded: expanded, scroll: inspectScroll })] })) : (_jsx(HotspotView, { agg: agg, sort: sort, width: columns - 4, height: ledgerRows + inspectorRows + 1, cursor: hotCursor, tick: tick, switchTick: switchTick })), _jsx(Box, { height: 1, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: "100%", height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, italic: true, wrap: "truncate", children: [_jsx(HintLine, { text: hints }), totals.tokens.input > 0 ? (_jsx(Text, { color: "subtle", children: ` ${formatTokens(totals.tokens.input)}→${formatTokens(totals.tokens.output)}` })) : ('')] }) })] }));
364
+ return (_jsxs(Box, { ref: ref, flexDirection: "column", width: "100%", paddingX: 1, children: [header, tabs, _jsx(WaveBand, { band: band, width: bandWidth, cursorColumn: columnOfIndex(band, indexes[clampedCursor] ?? 0), viewportStart: columnOfIndex(band, indexes[windowStart] ?? 0), viewportEnd: columnOfIndex(band, indexes[Math.min(filtered.length - 1, windowStart + ledgerRows - 1)] ?? 0), matches: matchColumns, tick: tick, alertTick: alertTick, onColumnClick: column => {
365
+ // 点击波形列(或标尺行)= 跳到该列最近事件;空列继承前驱的
366
+ // firstIndex,空档区点击落在空档开始处。查询过滤掉的行不跳。
367
+ const nodeIndex = band.buckets[column]?.firstIndex ?? -1;
368
+ if (nodeIndex < 0)
369
+ return;
370
+ const target = indexes.indexOf(nodeIndex);
371
+ if (target >= 0)
372
+ jumpTo(target);
373
+ } }), _jsx(Box, { height: 1, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx("ink-box", { style: { flexDirection: 'column', flexGrow: 1, flexShrink: 1, overflow: 'hidden', width: '100%' }, onWheel: handleWheel, children: view === 'timeline' ? (_jsxs(_Fragment, { children: [_jsx(Ledger, { rows: filtered, start: windowStart, height: ledgerRows, cursor: clampedCursor, width: columns - 4, tick: tick, arrivalTick: arrivalTick, arrivalFrom: arrivalFrom, onRowClick: index => jumpTo(index) }), _jsx(Divider, { color: "permission", width: bandWidth }), _jsx(Inspector, { node: focused, detail: detail, height: inspectorRows, width: columns - 4, expanded: expanded, scroll: inspectScroll })] })) : (_jsx(HotspotView, { agg: agg, sort: sort, width: columns - 4, height: ledgerRows + inspectorRows + 1, cursor: hotCursor, tick: tick, switchTick: switchTick, onRowClick: index => jumpFromHotspot(hotspotRows(agg)[index]) })) }), _jsx(Box, { height: 1, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: "100%", height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, italic: true, wrap: "truncate", children: [_jsx(HintLine, { text: hints }), totals.tokens.input > 0 ? (_jsx(Text, { color: "subtle", children: ` ${formatTokens(totals.tokens.input)}→${formatTokens(totals.tokens.output)}` })) : ('')] }) })] }));
307
374
  }