@deepseek-harness-tui/dsh-tui 0.9.3 → 0.10.0-beta.2

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 (267) hide show
  1. package/README.md +24 -6
  2. package/cordis.patch.yml +117 -39
  3. package/cordis.yml +13 -10
  4. package/lib/types/api.d.ts +7 -0
  5. package/lib/types/api.d.ts.map +1 -0
  6. package/lib/types/api.js +1 -0
  7. package/lib/types/cc/hyperlink.d.ts.map +1 -1
  8. package/lib/types/cc/hyperlink.js +42 -4
  9. package/lib/types/cc/markdown.d.ts.map +1 -1
  10. package/lib/types/cc/markdown.js +15 -5
  11. package/lib/types/commands.d.ts +6 -0
  12. package/lib/types/commands.d.ts.map +1 -1
  13. package/lib/types/commands.js +12 -8
  14. package/lib/types/components/MessageList.d.ts +8 -4
  15. package/lib/types/components/MessageList.d.ts.map +1 -1
  16. package/lib/types/components/MessageList.js +94 -32
  17. package/lib/types/components/PermissionsPicker.d.ts +6 -7
  18. package/lib/types/components/PermissionsPicker.d.ts.map +1 -1
  19. package/lib/types/components/PermissionsPicker.js +20 -14
  20. package/lib/types/components/PromptEditor.d.ts +46 -0
  21. package/lib/types/components/PromptEditor.d.ts.map +1 -0
  22. package/lib/types/components/PromptEditor.js +60 -0
  23. package/lib/types/components/PromptInput.d.ts +12 -0
  24. package/lib/types/components/PromptInput.d.ts.map +1 -1
  25. package/lib/types/components/PromptInput.js +1240 -205
  26. package/lib/types/components/RewindPicker.js +1 -1
  27. package/lib/types/components/SplitDiffView.d.ts +11 -1
  28. package/lib/types/components/SplitDiffView.d.ts.map +1 -1
  29. package/lib/types/components/SplitDiffView.js +9 -2
  30. package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
  31. package/lib/types/components/StreamingMarkdown.js +140 -3
  32. package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
  33. package/lib/types/components/SubagentDashboard.js +2 -2
  34. package/lib/types/components/SubagentDetailScene.js +1 -1
  35. package/lib/types/components/ThemePicker.d.ts +13 -15
  36. package/lib/types/components/ThemePicker.d.ts.map +1 -1
  37. package/lib/types/components/ThemePicker.js +24 -26
  38. package/lib/types/components/Tooltip.d.ts +67 -0
  39. package/lib/types/components/Tooltip.d.ts.map +1 -0
  40. package/lib/types/components/Tooltip.js +220 -0
  41. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
  42. package/lib/types/components/approvals/ApprovalPanel.js +1 -1
  43. package/lib/types/components/design-system/Divider.d.ts +14 -1
  44. package/lib/types/components/design-system/Divider.d.ts.map +1 -1
  45. package/lib/types/components/design-system/Divider.js +86 -8
  46. package/lib/types/components/design-system/ThemeProvider.d.ts +5 -2
  47. package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
  48. package/lib/types/components/design-system/ThemeProvider.js +52 -16
  49. package/lib/types/components/design-system/ThemedBox.d.ts +23 -2
  50. package/lib/types/components/design-system/ThemedBox.d.ts.map +1 -1
  51. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +11 -6
  52. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  53. package/lib/types/components/messages/AssistantThinkingMessage.js +10 -6
  54. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +13 -1
  55. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  56. package/lib/types/components/messages/AssistantToolUseMessage.js +37 -6
  57. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  58. package/lib/types/components/messages/UserPromptMessage.js +7 -1
  59. package/lib/types/components/questions/AskUserQuestionPanel.js +1 -1
  60. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  61. package/lib/types/components/questions/PlanReviewPanel.js +29 -3
  62. package/lib/types/components/sessions/SessionListRow.d.ts +8 -1
  63. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  64. package/lib/types/components/sessions/SessionListRow.js +9 -2
  65. package/lib/types/components/sessions/SessionPreview.d.ts.map +1 -1
  66. package/lib/types/components/sessions/SessionPreview.js +5 -3
  67. package/lib/types/components/sessions/WorkspaceListRow.d.ts +17 -0
  68. package/lib/types/components/sessions/WorkspaceListRow.d.ts.map +1 -0
  69. package/lib/types/components/sessions/WorkspaceListRow.js +27 -0
  70. package/lib/types/components/smoothReveal.d.ts +97 -0
  71. package/lib/types/components/smoothReveal.d.ts.map +1 -0
  72. package/lib/types/components/smoothReveal.js +279 -0
  73. package/lib/types/customTheme.d.ts +4 -0
  74. package/lib/types/customTheme.d.ts.map +1 -1
  75. package/lib/types/customTheme.js +9 -5
  76. package/lib/types/dsh-adapter/approvals.d.ts +114 -2
  77. package/lib/types/dsh-adapter/approvals.d.ts.map +1 -1
  78. package/lib/types/dsh-adapter/approvals.js +241 -3
  79. package/lib/types/dsh-adapter/channel.d.ts +59 -2
  80. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  81. package/lib/types/dsh-adapter/channel.js +499 -80
  82. package/lib/types/dsh-adapter/childStderr.d.ts.map +1 -1
  83. package/lib/types/dsh-adapter/childStderr.js +9 -0
  84. package/lib/types/dsh-adapter/contract.d.ts +17 -13
  85. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  86. package/lib/types/dsh-adapter/contract.js +36 -27
  87. package/lib/types/dsh-adapter/credentialRefGuard.d.ts +40 -0
  88. package/lib/types/dsh-adapter/credentialRefGuard.d.ts.map +1 -0
  89. package/lib/types/dsh-adapter/credentialRefGuard.js +43 -0
  90. package/lib/types/dsh-adapter/effect-ledger.d.ts +1 -1
  91. package/lib/types/dsh-adapter/effect-ledger.d.ts.map +1 -1
  92. package/lib/types/dsh-adapter/effect-ledger.js +1 -0
  93. package/lib/types/dsh-adapter/extensions.d.ts +4 -1
  94. package/lib/types/dsh-adapter/extensions.d.ts.map +1 -1
  95. package/lib/types/dsh-adapter/extensions.js +8 -1
  96. package/lib/types/dsh-adapter/index.d.ts +11 -1
  97. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  98. package/lib/types/dsh-adapter/index.js +2 -0
  99. package/lib/types/dsh-adapter/plugin-test-utils.d.ts +62 -0
  100. package/lib/types/dsh-adapter/plugin-test-utils.d.ts.map +1 -0
  101. package/lib/types/dsh-adapter/plugin-test-utils.js +61 -0
  102. package/lib/types/dsh-adapter/plugin.d.ts +20 -0
  103. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  104. package/lib/types/dsh-adapter/plugin.js +174 -39
  105. package/lib/types/dsh-adapter/plugins-info.d.ts.map +1 -1
  106. package/lib/types/dsh-adapter/plugins-info.js +12 -0
  107. package/lib/types/dsh-adapter/preset-resolution.d.ts +49 -0
  108. package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -0
  109. package/lib/types/dsh-adapter/preset-resolution.js +42 -0
  110. package/lib/types/dsh-adapter/presets.d.ts +0 -26
  111. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  112. package/lib/types/dsh-adapter/presets.js +4 -14
  113. package/lib/types/dsh-adapter/promptDebug.d.ts.map +1 -1
  114. package/lib/types/dsh-adapter/promptDebug.js +4 -1
  115. package/lib/types/dsh-adapter/providerGuard.d.ts +75 -0
  116. package/lib/types/dsh-adapter/providerGuard.d.ts.map +1 -0
  117. package/lib/types/dsh-adapter/providerGuard.js +104 -0
  118. package/lib/types/dsh-adapter/questions-answerer.d.ts +49 -0
  119. package/lib/types/dsh-adapter/questions-answerer.d.ts.map +1 -0
  120. package/lib/types/dsh-adapter/questions-answerer.js +61 -0
  121. package/lib/types/dsh-adapter/recap.d.ts.map +1 -1
  122. package/lib/types/dsh-adapter/recap.js +12 -6
  123. package/lib/types/dsh-adapter/sanitize.d.ts.map +1 -1
  124. package/lib/types/dsh-adapter/sanitize.js +6 -1
  125. package/lib/types/dsh-adapter/sessions/digest.d.ts +21 -1
  126. package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -1
  127. package/lib/types/dsh-adapter/sessions/digest.js +229 -3
  128. package/lib/types/dsh-adapter/sessions/frames.d.ts +3 -0
  129. package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -1
  130. package/lib/types/dsh-adapter/sessions/frames.js +34 -19
  131. package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -1
  132. package/lib/types/dsh-adapter/sessions/list.js +148 -31
  133. package/lib/types/dsh-adapter/sessions/store.d.ts +8 -0
  134. package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -1
  135. package/lib/types/dsh-adapter/sessions/store.js +24 -4
  136. package/lib/types/dsh-adapter/sessions/types.d.ts +3 -0
  137. package/lib/types/dsh-adapter/sessions/types.d.ts.map +1 -1
  138. package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -1
  139. package/lib/types/dsh-adapter/settings-sections.js +13 -1
  140. package/lib/types/dsh-adapter/settingsEditor.d.ts +6 -0
  141. package/lib/types/dsh-adapter/settingsEditor.d.ts.map +1 -1
  142. package/lib/types/dsh-adapter/settingsEditor.js +15 -3
  143. package/lib/types/dsh-adapter/subagents.d.ts +4 -0
  144. package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
  145. package/lib/types/dsh-adapter/subagents.js +8 -0
  146. package/lib/types/dsh-adapter/themes.d.ts +51 -0
  147. package/lib/types/dsh-adapter/themes.d.ts.map +1 -0
  148. package/lib/types/dsh-adapter/themes.js +272 -0
  149. package/lib/types/dsh-adapter/toast.d.ts +67 -0
  150. package/lib/types/dsh-adapter/toast.d.ts.map +1 -0
  151. package/lib/types/dsh-adapter/toast.js +141 -0
  152. package/lib/types/dsh-adapter/types.d.ts +1 -1
  153. package/lib/types/extensions.d.ts +4 -0
  154. package/lib/types/extensions.d.ts.map +1 -1
  155. package/lib/types/extensions.js +5 -1
  156. package/lib/types/history.d.ts.map +1 -1
  157. package/lib/types/history.js +7 -2
  158. package/lib/types/hooks/useRuntimeThemeSnapshot.d.ts +10 -0
  159. package/lib/types/hooks/useRuntimeThemeSnapshot.d.ts.map +1 -0
  160. package/lib/types/hooks/useRuntimeThemeSnapshot.js +32 -0
  161. package/lib/types/i18n.d.ts +199 -41
  162. package/lib/types/i18n.d.ts.map +1 -1
  163. package/lib/types/i18n.js +82 -20
  164. package/lib/types/ink/components/App.d.ts +20 -0
  165. package/lib/types/ink/components/App.d.ts.map +1 -1
  166. package/lib/types/ink/components/App.js +142 -13
  167. package/lib/types/ink/components/Box.d.ts +39 -2
  168. package/lib/types/ink/components/Box.d.ts.map +1 -1
  169. package/lib/types/ink/components/Box.js +61 -37
  170. package/lib/types/ink/events/context-menu-event.d.ts +19 -0
  171. package/lib/types/ink/events/context-menu-event.d.ts.map +1 -0
  172. package/lib/types/ink/events/context-menu-event.js +20 -0
  173. package/lib/types/ink/events/drag-event.d.ts +44 -0
  174. package/lib/types/ink/events/drag-event.d.ts.map +1 -0
  175. package/lib/types/ink/events/drag-event.js +39 -0
  176. package/lib/types/ink/events/event-handlers.d.ts +8 -0
  177. package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
  178. package/lib/types/ink/events/event-handlers.js +8 -0
  179. package/lib/types/ink/hit-test.d.ts +69 -0
  180. package/lib/types/ink/hit-test.d.ts.map +1 -1
  181. package/lib/types/ink/hit-test.js +260 -6
  182. package/lib/types/ink/ink.d.ts +25 -0
  183. package/lib/types/ink/ink.d.ts.map +1 -1
  184. package/lib/types/ink/ink.js +74 -10
  185. package/lib/types/ink/parse-keypress.d.ts +20 -0
  186. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  187. package/lib/types/ink/parse-keypress.js +190 -22
  188. package/lib/types/ink/renderer.d.ts.map +1 -1
  189. package/lib/types/ink/renderer.js +15 -1
  190. package/lib/types/ink/screen.d.ts +7 -0
  191. package/lib/types/ink/screen.d.ts.map +1 -1
  192. package/lib/types/ink/screen.js +8 -1
  193. package/lib/types/ink/termio/osc.d.ts +2 -1
  194. package/lib/types/ink/termio/osc.d.ts.map +1 -1
  195. package/lib/types/ink/termio/osc.js +26 -5
  196. package/lib/types/plugin-spec/registry.d.ts +1 -1
  197. package/lib/types/plugin-spec/registry.js +1 -1
  198. package/lib/types/presetPrefs.d.ts +6 -5
  199. package/lib/types/presetPrefs.d.ts.map +1 -1
  200. package/lib/types/presetPrefs.js +10 -5
  201. package/lib/types/screens/Chat.d.ts +4 -1
  202. package/lib/types/screens/Chat.d.ts.map +1 -1
  203. package/lib/types/screens/Chat.js +169 -68
  204. package/lib/types/screens/SessionBrowser.d.ts +9 -4
  205. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  206. package/lib/types/screens/SessionBrowser.js +483 -75
  207. package/lib/types/screens/SessionTree.d.ts.map +1 -1
  208. package/lib/types/screens/SessionTree.js +5 -1
  209. package/lib/types/screens/Settings.d.ts.map +1 -1
  210. package/lib/types/screens/Settings.js +5 -1
  211. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  212. package/lib/types/screens/StatusLine.js +5 -1
  213. package/lib/types/screens/chatOverlay.d.ts +3 -2
  214. package/lib/types/screens/chatOverlay.d.ts.map +1 -1
  215. package/lib/types/screens/chatOverlay.js +2 -0
  216. package/lib/types/sessionPins.d.ts +33 -0
  217. package/lib/types/sessionPins.d.ts.map +1 -0
  218. package/lib/types/sessionPins.js +181 -0
  219. package/lib/types/sessions/format.d.ts +2 -0
  220. package/lib/types/sessions/format.d.ts.map +1 -1
  221. package/lib/types/sessions/format.js +9 -0
  222. package/lib/types/sessions/view.d.ts +42 -6
  223. package/lib/types/sessions/view.d.ts.map +1 -1
  224. package/lib/types/sessions/view.js +133 -35
  225. package/lib/types/test-utils.d.ts +2 -0
  226. package/lib/types/test-utils.d.ts.map +1 -0
  227. package/lib/types/test-utils.js +8 -0
  228. package/lib/types/theme.d.ts +22 -5
  229. package/lib/types/theme.d.ts.map +1 -1
  230. package/lib/types/theme.js +57 -7
  231. package/lib/types/themeCatalog.d.ts +36 -0
  232. package/lib/types/themeCatalog.d.ts.map +1 -0
  233. package/lib/types/themeCatalog.js +157 -0
  234. package/lib/types/tips.d.ts.map +1 -1
  235. package/lib/types/tips.js +114 -18
  236. package/lib/types/update.d.ts +159 -3
  237. package/lib/types/update.d.ts.map +1 -1
  238. package/lib/types/update.js +497 -16
  239. package/lib/types/utils/debug.d.ts.map +1 -1
  240. package/lib/types/utils/debug.js +2 -1
  241. package/lib/types/utils/keymap.d.ts +1 -1
  242. package/lib/types/utils/keymap.d.ts.map +1 -1
  243. package/lib/types/utils/keymap.js +19 -2
  244. package/lib/types/utils/urlGuard.d.ts +28 -0
  245. package/lib/types/utils/urlGuard.d.ts.map +1 -0
  246. package/lib/types/utils/urlGuard.js +21 -0
  247. package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +3 -0
  248. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +7 -11
  249. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +16 -1
  250. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +23 -2
  251. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +5 -5
  252. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/pi-ai.d.ts +23 -0
  253. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/pi-ai.js +27 -0
  254. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +17 -3
  255. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +36 -4
  256. package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +11 -12
  257. package/package.json +114 -74
  258. package/lib/types/dsh-adapter/packaged-skills.d.ts +0 -19
  259. package/lib/types/dsh-adapter/packaged-skills.d.ts.map +0 -1
  260. package/lib/types/dsh-adapter/packaged-skills.js +0 -78
  261. package/skills/audit/SKILL.md +0 -24
  262. package/skills/bug/SKILL.md +0 -27
  263. package/skills/pr-comments/SKILL.md +0 -20
  264. package/skills/practice/SKILL.md +0 -22
  265. package/skills/release-notes/SKILL.md +0 -25
  266. package/skills/review/SKILL.md +0 -25
  267. package/skills/vuln-check/SKILL.md +0 -25
@@ -0,0 +1,220 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import stripAnsi from 'strip-ansi';
4
+ import { Box, Text, useTerminalSize } from '../ui.js';
5
+ import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js';
6
+ import { stringWidth } from '../ink/stringWidth.js';
7
+ import { getGraphemeSegmenter } from '../utils/intl.js';
8
+ /**
9
+ * Tooltip hover system — the terminal equivalent of the HTML `title`
10
+ * attribute. Attach {@link useTooltip} props to any Box whose visible text
11
+ * may be truncated (tool-call header paths, wrapped prompts, status-line
12
+ * fields); after a ~600ms dwell the full content pops up in a floating card
13
+ * anchored at the pointer, and disappears the moment the pointer leaves.
14
+ *
15
+ * Hover events only exist in fullscreen (alternate screen + mouse
16
+ * tracking); inline mode never fires the handlers, so the store can only
17
+ * be written from a live fullscreen tree.
18
+ */
19
+ /** Default dwell before the tooltip appears (GUI `title` convention). */
20
+ const DEFAULT_TOOLTIP_DELAY_MS = 600;
21
+ // Module-level store (the `src/ink/instances.ts` single-instance pattern):
22
+ // writers are the hover handlers, the single reader is TooltipLayer.
23
+ let state = null;
24
+ let invalidationGeneration = 0;
25
+ const listeners = new Set();
26
+ function notify() {
27
+ for (const listener of listeners)
28
+ listener();
29
+ }
30
+ /** Subscribe to tooltip show/hide; returns an unsubscribe function. */
31
+ export function subscribeTooltip(listener) {
32
+ listeners.add(listener);
33
+ return () => {
34
+ listeners.delete(listener);
35
+ };
36
+ }
37
+ /** Stable snapshot for useSyncExternalStore (identity kept until changed). */
38
+ export function getTooltipSnapshot() {
39
+ return state;
40
+ }
41
+ function showTooltip(owner, col, row, content) {
42
+ state = { owner, anchorCol: col, anchorRow: row, content };
43
+ notify();
44
+ }
45
+ function hideTooltip(owner) {
46
+ if (state !== null && state.owner === owner) {
47
+ state = null;
48
+ notify();
49
+ }
50
+ }
51
+ /** Hide shown content and invalidate every pending dwell timer. */
52
+ export function clearTooltip() {
53
+ invalidationGeneration++;
54
+ if (state !== null) {
55
+ state = null;
56
+ notify();
57
+ }
58
+ }
59
+ /**
60
+ * Hover props that pop a tooltip with the full content after a dwell.
61
+ *
62
+ * The pointer position (screen col/row) is taken from the PointerEvent the
63
+ * renderer hands to `onMouseEnter` — the runtime always passes one even
64
+ * though the prop type is `() => void`, which is why the handler's parameter
65
+ * is optional (a `() => void` prop accepts a function with only optional
66
+ * parameters).
67
+ */
68
+ export function useTooltip(content, opts) {
69
+ const ownerRef = React.useRef(null);
70
+ if (ownerRef.current === null)
71
+ ownerRef.current = { timer: null };
72
+ const owner = ownerRef.current;
73
+ // Resolve function content at show time so callers can pass getters that
74
+ // read live channel state without re-binding the handlers.
75
+ const contentRef = React.useRef(content);
76
+ contentRef.current = content;
77
+ const delayMs = opts?.delayMs ?? DEFAULT_TOOLTIP_DELAY_MS;
78
+ React.useEffect(() => () => {
79
+ // Unmount clears our pending timer and our tooltip — a screen swap must
80
+ // not leave a ghost anchored to geometry that no longer exists.
81
+ if (owner.timer !== null)
82
+ clearTimeout(owner.timer);
83
+ hideTooltip(owner);
84
+ }, [owner]);
85
+ return React.useMemo(() => ({
86
+ onMouseEnter: (event) => {
87
+ const col = event?.col ?? 0;
88
+ const row = event?.row ?? 0;
89
+ if (owner.timer !== null)
90
+ clearTimeout(owner.timer);
91
+ const generationAtEnter = invalidationGeneration;
92
+ owner.timer = setTimeout(() => {
93
+ owner.timer = null;
94
+ if (generationAtEnter !== invalidationGeneration)
95
+ return;
96
+ const resolved = typeof contentRef.current === 'function'
97
+ ? contentRef.current()
98
+ : contentRef.current;
99
+ if (resolved === '')
100
+ return;
101
+ showTooltip(owner, col, row, resolved);
102
+ }, delayMs);
103
+ },
104
+ onMouseLeave: () => {
105
+ if (owner.timer !== null) {
106
+ clearTimeout(owner.timer);
107
+ owner.timer = null;
108
+ }
109
+ hideTooltip(owner);
110
+ },
111
+ }), [owner, delayMs]);
112
+ }
113
+ /**
114
+ * Convenience wrapper for list rows built from data (a component boundary
115
+ * makes the hook call legal per item). Renders a plain Box carrying the
116
+ * hover props around `children`.
117
+ */
118
+ export function TooltipTarget({ content, children, }) {
119
+ const hover = useTooltip(content);
120
+ return _jsx(Box, { ...hover, children: children });
121
+ }
122
+ /**
123
+ * The singleton floating layer: subscribes to the tooltip store and renders
124
+ * the rounded card above the anchor row (below it when there is no room),
125
+ * horizontally clamped inside the terminal. Mount once, LAST in the tree so
126
+ * it paints over everything else. Terminal resize hides the tooltip — its
127
+ * anchor is screen geometry that just went stale.
128
+ */
129
+ export function TooltipLayer({ invalidationKey, subscribeInvalidation, } = {}) {
130
+ const { columns, rows } = useTerminalSize();
131
+ const terminalFocused = useTerminalFocus();
132
+ const tooltip = React.useSyncExternalStore(subscribeTooltip, getTooltipSnapshot);
133
+ // Remounts and modal/screen transitions invalidate shown AND pending tips.
134
+ React.useEffect(() => {
135
+ clearTooltip();
136
+ }, [invalidationKey]);
137
+ React.useEffect(() => {
138
+ if (!terminalFocused)
139
+ clearTooltip();
140
+ }, [terminalFocused]);
141
+ React.useEffect(() => {
142
+ if (subscribeInvalidation === undefined)
143
+ return;
144
+ return subscribeInvalidation(clearTooltip);
145
+ }, [subscribeInvalidation]);
146
+ const prevSize = React.useRef({ columns, rows });
147
+ React.useEffect(() => {
148
+ if (prevSize.current.columns !== columns || prevSize.current.rows !== rows) {
149
+ prevSize.current = { columns, rows };
150
+ clearTooltip();
151
+ }
152
+ }, [columns, rows]);
153
+ if (tooltip === null)
154
+ return null;
155
+ // A bordered card needs at least one content cell plus two border cells.
156
+ // Below that, hiding is safer than creating geometry wider/taller than the
157
+ // terminal (Yoga would clip unpredictably on 1–2 column/row resize states).
158
+ if (columns < 3 || rows < 3)
159
+ return null;
160
+ const maxWidth = Math.max(3, columns - 4);
161
+ // Grapheme-aware wrap (parity with PromptInput's wrapToWidth): code-point
162
+ // iteration splits ZWJ emoji and combining sequences across rows, leaving
163
+ // broken halves at the row edges. Newlines are honoured like wrapWidth.
164
+ let lines = wrapTooltipContent(tooltip.content, maxWidth - 2);
165
+ // A tooltip taller than the terminal is useless; keep the head.
166
+ const maxLines = Math.max(1, rows - 2);
167
+ if (lines.length > maxLines)
168
+ lines = lines.slice(0, maxLines);
169
+ const innerWidth = Math.min(maxWidth - 2, Math.max(1, ...lines.map(stringWidth)));
170
+ const width = innerWidth + 2;
171
+ const height = lines.length + 2;
172
+ // Prefer resting on the row ABOVE the anchor; when there is no room
173
+ // (anchor at the top of the screen), drop below it, clamped on-screen.
174
+ const topAbove = tooltip.anchorRow - height;
175
+ const top = topAbove >= 0
176
+ ? topAbove
177
+ : Math.max(0, Math.min(tooltip.anchorRow + 1, rows - height));
178
+ const left = Math.max(0, Math.min(tooltip.anchorCol, columns - width));
179
+ return (_jsx(Box, { position: "absolute", left: left, top: top, width: width, height: height, flexDirection: "column", flexShrink: 0, borderStyle: "round", borderColor: "permission", backgroundColor: "toolCardBackground", children: lines.map((line, index) => (_jsx(Text, { wrap: "truncate-end", children: line === '' ? ' ' : line }, index))) }));
180
+ }
181
+ /**
182
+ * Wrap tooltip content to display width, breaking only BETWEEN grapheme
183
+ * clusters (ZWJ emoji, combining sequences and CJK wide cells stay whole).
184
+ * Newlines in the input are honoured. Mirrors PromptInput.wrapToWidth's
185
+ * break rule so the tooltip shows exactly the text the input would wrap.
186
+ */
187
+ function wrapTooltipContent(text, width) {
188
+ const rows = [];
189
+ if (width <= 0)
190
+ return rows;
191
+ // Tooltip content is display-only: remove ANSI/C0 controls before grapheme
192
+ // segmentation so a CSI sequence cannot be split and counted as visible
193
+ // columns. Newlines remain structural; tabs become deterministic spaces.
194
+ text = stripAnsi(text)
195
+ .replace(/\t/gu, ' ')
196
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, '');
197
+ const segmenter = getGraphemeSegmenter();
198
+ for (const line of text.split('\n')) {
199
+ if (line === '') {
200
+ rows.push('');
201
+ continue;
202
+ }
203
+ let current = '';
204
+ let used = 0;
205
+ for (const { segment } of segmenter.segment(line)) {
206
+ const w = stringWidth(segment);
207
+ if (used + w > width && current !== '') {
208
+ rows.push(current);
209
+ current = segment;
210
+ used = w;
211
+ }
212
+ else {
213
+ current += segment;
214
+ used += w;
215
+ }
216
+ }
217
+ rows.push(current);
218
+ }
219
+ return rows;
220
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ApprovalPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/approvals/ApprovalPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AAEtE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,cAAc,GAAG,UAAU,KAAK,IAAI,CAAA;CAClE,CAAA;AAID,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,kBAAkB,GAAG,KAAK,CAAC,SAAS,CA+EzF"}
1
+ {"version":3,"file":"ApprovalPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/approvals/ApprovalPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AAEtE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,cAAc,GAAG,UAAU,KAAK,IAAI,CAAA;CAClE,CAAA;AAID,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,kBAAkB,GAAG,KAAK,CAAC,SAAS,CAkFzF"}
@@ -45,7 +45,7 @@ export function ApprovalPanel({ approval, onDecide }) {
45
45
  }
46
46
  }, { isActive: true });
47
47
  const optionLabels = [t('approval-yes'), t('approval-no')];
48
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2, paddingRight: 2, width: "100%", children: [_jsx(Divider, { color: "permission", title: t('approval-waiting', { tool: approval.toolName }), padding: 4 }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [approval.command !== undefined && (_jsx(Box, { flexDirection: "column", paddingX: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: approval.command }) })), approval.reason !== undefined && (_jsx(Text, { dimColor: true, wrap: "wrap", children: approval.reason })), _jsx(Text, { dimColor: true, children: t('approval-proceed') })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: optionLabels.map((label, index) => {
48
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2, paddingRight: 2, width: "100%", children: [_jsx(Divider, { color: "permission", title: t('approval-waiting', { tool: approval.toolName }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [approval.external === true && (_jsxs(Text, { color: "warning", wrap: "wrap", children: ["[external] ", t('approval-external-hint')] })), approval.command !== undefined && (_jsx(Box, { flexDirection: "column", paddingX: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: approval.command }) })), approval.reason !== undefined && (_jsx(Text, { dimColor: true, wrap: "wrap", children: approval.reason })), _jsx(Text, { dimColor: true, children: t('approval-proceed') })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: optionLabels.map((label, index) => {
49
49
  const focused = index === focusIndex;
50
50
  const hovered = index === hoverIndex;
51
51
  return (_jsxs(Box, { flexDirection: "row", marginTop: focused ? 1 : 0, onClick: () => onDecide(OUTCOMES[index]), onMouseEnter: () => setHoverIndex(index), onMouseLeave: () => setHoverIndex(current => (current === index ? -1 : current)), backgroundColor: hovered && !focused ? 'userMessageBackgroundHover' : undefined, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: focused ? 'claude' : undefined, bold: focused, children: focused ? POINTER : ' ' }) }), _jsxs(Text, { bold: focused, color: focused ? 'claude' : undefined, wrap: "wrap", children: [index + 1, ". ", label] })] }, label));
@@ -3,7 +3,7 @@ import type { Theme } from '../../theme.js';
3
3
  type DividerProps = {
4
4
  /**
5
5
  * Width of the divider in characters.
6
- * Defaults to terminal width.
6
+ * Defaults to the available layout width.
7
7
  */
8
8
  width?: number;
9
9
  /**
@@ -31,6 +31,19 @@ type DividerProps = {
31
31
  * A horizontal divider line, optionally with a title in the middle
32
32
  * (in the Claude Code visual language).
33
33
  *
34
+ * The rule fills the width Yoga actually grants it: the Box is measured
35
+ * after layout (SearchBox pattern — a resize re-layouts without any prop
36
+ * change, so measure on every commit) and the terminal column count only
37
+ * seeds the first frame. A full-terminal-width rule nested in a narrower
38
+ * container (e.g., the transcript beside the 2-column timeline rail)
39
+ * would otherwise wrap onto a second row.
40
+ *
41
+ * Because the rendered rule width feeds back into the width Yoga grants
42
+ * the Box, the measurement negotiation is bounded per terminal-width
43
+ * generation (MAX_APPLIED_MEASUREMENTS below): a non-converging layout
44
+ * degrades to a frozen, truncated rule instead of tripping React's max
45
+ * update depth (error #185, a hard process crash).
46
+ *
34
47
  * @example
35
48
  * // ─────────── Title ───────────
36
49
  * <Divider title="Title" />
@@ -1 +1 @@
1
- {"version":3,"file":"Divider.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/Divider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAE3C,KAAK,YAAY,GAAG;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,KAAK,CAAA;IAEnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,KAAK,EACL,IAAU,EACV,OAAW,EACX,KAAK,GACN,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAwBhC"}
1
+ {"version":3,"file":"Divider.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/Divider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4C,MAAM,OAAO,CAAA;AAOhE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAE3C,KAAK,YAAY,GAAG;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,KAAK,CAAA;IAEnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AASD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,KAAK,EACL,IAAU,EACV,OAAW,EACX,KAAK,GACN,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAqEhC"}
@@ -1,24 +1,102 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState } from 'react';
3
+ import Box from '../../ink/components/Box.js';
2
4
  import Text from '../../ink/components/Text.js';
5
+ import measureElement from '../../ink/measure-element.js';
3
6
  import { stringWidth } from '../../ink/stringWidth.js';
4
7
  import { useTerminalSize } from '../../ink/hooks/use-terminal-size.js';
8
+ // Upper bound on distinct measurements applied per terminal-width
9
+ // generation. Converging layouts apply one or two; a non-converging
10
+ // context (content-sized Box, or a sibling negotiating space the same
11
+ // way) would keep changing the measured width once per commit forever,
12
+ // so freeze well before the reconciler's 50-nested-update limit.
13
+ const MAX_APPLIED_MEASUREMENTS = 8;
5
14
  /**
6
15
  * A horizontal divider line, optionally with a title in the middle
7
16
  * (in the Claude Code visual language).
8
17
  *
18
+ * The rule fills the width Yoga actually grants it: the Box is measured
19
+ * after layout (SearchBox pattern — a resize re-layouts without any prop
20
+ * change, so measure on every commit) and the terminal column count only
21
+ * seeds the first frame. A full-terminal-width rule nested in a narrower
22
+ * container (e.g., the transcript beside the 2-column timeline rail)
23
+ * would otherwise wrap onto a second row.
24
+ *
25
+ * Because the rendered rule width feeds back into the width Yoga grants
26
+ * the Box, the measurement negotiation is bounded per terminal-width
27
+ * generation (MAX_APPLIED_MEASUREMENTS below): a non-converging layout
28
+ * degrades to a frozen, truncated rule instead of tripping React's max
29
+ * update depth (error #185, a hard process crash).
30
+ *
9
31
  * @example
10
32
  * // ─────────── Title ───────────
11
33
  * <Divider title="Title" />
12
34
  */
13
35
  export function Divider({ width, color, char = '─', padding = 0, title, }) {
14
36
  const { columns } = useTerminalSize();
15
- const lineWidth = Math.max(0, (width ?? columns) - padding);
37
+ const [measuredWidth, setMeasuredWidth] = useState(null);
38
+ const boxRef = useRef(null);
39
+ // Measurement feedback guard: the rule width rendered from
40
+ // `measuredWidth` feeds back into the width Yoga grants the Box, so
41
+ // the measured value can drift or ping-pong without ever converging —
42
+ // one setState per commit then trips React's max update depth (error
43
+ // #185) and kills the process. Apply at most MAX_APPLIED_MEASUREMENTS
44
+ // distinct widths per terminal-width generation and freeze once a
45
+ // value repeats (A→B→A). truncate-end keeps an over-wide frozen rule
46
+ // on one row, so freezing degrades gracefully; a resize (columns
47
+ // change) reopens the negotiation.
48
+ const negotiation = useRef({ columns, applied: [], frozen: false });
49
+ useLayoutEffect(() => {
50
+ const node = boxRef.current;
51
+ if (!node)
52
+ return;
53
+ const state = negotiation.current;
54
+ if (state.columns !== columns) {
55
+ state.columns = columns;
56
+ state.applied = [];
57
+ state.frozen = false;
58
+ // The current Yoga layout was rendered from the previous terminal
59
+ // width. Clear it and let the next render seed from the new columns;
60
+ // measuring this stale pass would keep a grown terminal permanently
61
+ // pinned to its old narrow width.
62
+ setMeasuredWidth(null);
63
+ return;
64
+ }
65
+ if (state.frozen)
66
+ return;
67
+ const w = measureElement(node).width;
68
+ if (w <= 0)
69
+ return;
70
+ const applied = state.applied;
71
+ if (w === applied[applied.length - 1])
72
+ return;
73
+ if (applied.includes(w) || applied.length >= MAX_APPLIED_MEASUREMENTS) {
74
+ state.frozen = true;
75
+ return;
76
+ }
77
+ applied.push(w);
78
+ setMeasuredWidth(w);
79
+ });
80
+ const available = measuredWidth ?? columns;
81
+ const lineWidth = Math.max(0, Math.min(available, width ?? columns) - padding);
16
82
  const titleWidth = title ? stringWidth(title) : 0;
17
- if (title && titleWidth < lineWidth) {
18
- const lineLength = lineWidth - titleWidth;
19
- const leftLength = Math.floor(lineLength / 2);
20
- const rightLength = Math.ceil(lineLength / 2);
21
- return (_jsxs(Text, { dimColor: !color, color: color, children: [char.repeat(leftLength), title, char.repeat(rightLength)] }));
83
+ let text;
84
+ if (title) {
85
+ if (titleWidth < lineWidth) {
86
+ const lineLength = lineWidth - titleWidth;
87
+ const leftLength = Math.floor(lineLength / 2);
88
+ const rightLength = Math.ceil(lineLength / 2);
89
+ text = char.repeat(leftLength) + title + char.repeat(rightLength);
90
+ }
91
+ else {
92
+ // Title wider than the line (long turn-error notices on narrow
93
+ // windows): keep the message — truncate-end clips it to the
94
+ // available width instead of dropping it for a bare rule.
95
+ text = title;
96
+ }
97
+ }
98
+ else {
99
+ text = char.repeat(lineWidth);
22
100
  }
23
- return (_jsx(Text, { dimColor: !color, color: color, children: char.repeat(lineWidth) }));
101
+ return (_jsx(Box, { ref: boxRef, children: _jsx(Text, { dimColor: !color, color: color, wrap: "truncate-end", children: text }) }));
24
102
  }
@@ -1,14 +1,17 @@
1
1
  import React from 'react';
2
+ import type { TuiThemeHost } from '../../dsh-adapter/themes.js';
2
3
  /**
3
4
  * DSH_TUI_THEME skips terminal detection (tests, debugging). Accepts a
4
- * built-in name (auto|light|dark|dark-ansi) or a user theme name; invalid
5
+ * built-in, static JSON, or runtime plugin theme name; invalid
5
6
  * values are warned and ignored by the caller, falling back to detection.
6
7
  * Exported for /reload, which must respect the env override's precedence.
7
8
  */
8
9
  export declare function envThemeOverride(): string | undefined;
9
- export declare function ThemeProvider({ children, theme, }: {
10
+ export declare function ThemeProvider({ children, theme, themeHost, }: {
10
11
  children: React.ReactNode;
11
12
  theme?: string;
13
+ /** Optional runtime theme host; absent in headless/static embeds. */
14
+ themeHost?: TuiThemeHost;
12
15
  }): React.ReactNode;
13
16
  /**
14
17
  * Resolves the active theme name and the runtime setter. Returns
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/ThemeProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyD,MAAM,OAAO,CAAA;AAsE7E;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAGrD;AAcD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,GAAG,KAAK,CAAC,SAAS,CAuIlB;AAED;;;GAGG;AACH,wBAAgB,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAG9D"}
1
+ {"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/ThemeProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyD,MAAM,OAAO,CAAA;AAU7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAgE/D;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAGrD;AAcD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,KAAK,EACL,SAAS,GACV,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qEAAqE;IACrE,SAAS,CAAC,EAAE,YAAY,CAAA;CACzB,GAAG,KAAK,CAAC,SAAS,CA2KlB;AAED;;;GAGG;AACH,wBAAgB,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAG9D"}
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { createContext, useContext, useEffect, useState } from 'react';
3
- import { registerCustomThemeResolver, setActiveThemeName, setAutoThemeBase, getAutoThemeBase, AUTO_THEME_NAME, } from '../../theme.js';
4
- import { isThemeAvailable, resolveCustomTheme } from '../../customTheme.js';
3
+ import { isThemeAvailable, registerCustomThemeResolver, setActiveThemeName, setAutoThemeBase, getAutoThemeBase, AUTO_THEME_NAME, } from '../../theme.js';
4
+ import { resolveCustomTheme } from '../../customTheme.js';
5
+ import { useRuntimeThemeSnapshot } from '../../hooks/useRuntimeThemeSnapshot.js';
5
6
  import { readThemePref, writeThemePref } from '../../themePrefs.js';
6
7
  import useStdin from '../../ink/hooks/use-stdin.js';
7
8
  import { oscColor } from '../../ink/terminal-querier.js';
@@ -13,8 +14,8 @@ import { logForDebugging } from '../../utils/debug.js';
13
14
  * (~/.dsh-tui/theme.json), it queries the terminal's background color
14
15
  * (OSC 11) before first paint and picks the Gentle Mist Blue `light` palette
15
16
  * on light backgrounds, `dark` otherwise. Priority: explicit `theme` prop >
16
- * DSH_TUI_THEME (built-in or user theme name) > persisted `/theme` choice >
17
- * OSC 11 detection. An invalid forced name is warned and skipped, so
17
+ * DSH_TUI_THEME (built-in, static, or runtime plugin name) > persisted `/theme`
18
+ * choice > OSC 11 detection. An invalid forced name is warned and skipped, so
18
19
  * detection still runs. Children render only after the theme settles, so
19
20
  * the first frame already carries the final palette — no dark→light flash.
20
21
  * Detection never blocks boot: a terminal that ignores OSC 11 (or a 400ms
@@ -31,12 +32,13 @@ import { logForDebugging } from '../../utils/debug.js';
31
32
  * system light/dark mode.
32
33
  *
33
34
  * The context also exposes setTheme() for the runtime `/theme` picker: it
34
- * validates the name, persists the choice to ~/.dsh-tui/theme.json and hot
35
- * swaps the palette (and the module-level mirror) immediately.
35
+ * validates static or plugin names, persists the choice to ~/.dsh-tui/theme.json
36
+ * and hot swaps the palette (and the module-level mirror) immediately. A
37
+ * disappearing plugin theme falls back safely without erasing the request.
36
38
  */
37
- // User themes (~/.dsh-tui/themes/<name>.json) resolve through this registry,
38
- // so getTheme() serves them to every themed component and to non-React
39
- // rendering (markdown inline code) without a context.
39
+ // Static user themes (~/.dsh-tui/themes/<name>.json) resolve through this
40
+ // registry; the optional runtime host resolver is installed by the Cordis
41
+ // service. Together they serve every themed component and non-React rendering.
40
42
  registerCustomThemeResolver(resolveCustomTheme);
41
43
  const ThemeContext = createContext({
42
44
  theme: 'dark',
@@ -45,7 +47,7 @@ const ThemeContext = createContext({
45
47
  });
46
48
  /**
47
49
  * DSH_TUI_THEME skips terminal detection (tests, debugging). Accepts a
48
- * built-in name (auto|light|dark|dark-ansi) or a user theme name; invalid
50
+ * built-in, static JSON, or runtime plugin theme name; invalid
49
51
  * values are warned and ignored by the caller, falling back to detection.
50
52
  * Exported for /reload, which must respect the env override's precedence.
51
53
  */
@@ -63,7 +65,8 @@ const DETECT_TIMEOUT_MS = 400;
63
65
  function isLightBackground(r, g, b) {
64
66
  return 0.299 * r + 0.587 * g + 0.114 * b > 140;
65
67
  }
66
- export function ThemeProvider({ children, theme, }) {
68
+ export function ThemeProvider({ children, theme, themeHost, }) {
69
+ const runtimeThemeSnapshot = useRuntimeThemeSnapshot(themeHost);
67
70
  // Resolution happens once on mount: the forced chain (prop > env >
68
71
  // persisted) or null, which arms OSC 11 detection.
69
72
  const [forced] = useState(() => theme ?? envThemeOverride() ?? readThemePref());
@@ -72,12 +75,16 @@ export function ThemeProvider({ children, theme, }) {
72
75
  return false;
73
76
  if (isThemeAvailable(forced))
74
77
  return true;
75
- console.warn(`[dsh-tui] theme "${forced}" not found (built-ins: auto, light, dark, dark-ansi; user themes: ~/.dsh-tui/themes/*.json); falling back to auto-detection`);
78
+ console.warn(`[dsh-tui] theme "${forced}" not found (built-ins: auto, light, dark, dark-ansi; static ~/.dsh-tui/themes/*.json; runtime plugin themes); falling back to auto-detection`);
76
79
  return false;
77
80
  });
78
81
  // `auto` (like the unforced path) stays null until detection settles, so
79
82
  // the first frame already carries the detected palette.
80
83
  const [active, setActive] = useState(forcedValid && forced !== AUTO_THEME_NAME ? forced ?? null : null);
84
+ // Keep the user's requested name independent from the rendered fallback. If
85
+ // a plugin theme disappears, this lets a later registration restore it while
86
+ // the persisted preference remains untouched.
87
+ const requestedThemeRef = React.useRef(forced);
81
88
  const { internal_querier, isRawModeSupported } = useStdin();
82
89
  /**
83
90
  * The palette `auto` resolves to, as React state so a flip re-renders
@@ -97,7 +104,11 @@ export function ThemeProvider({ children, theme, }) {
97
104
  // directly; `auto` records the base and activates as `auto`.
98
105
  const settle = (name, why) => {
99
106
  logForDebugging(`theme: ${name} (${why})`);
100
- if (forced === AUTO_THEME_NAME) {
107
+ const requested = requestedThemeRef.current;
108
+ if (forced !== AUTO_THEME_NAME && requested !== undefined && isThemeAvailable(requested)) {
109
+ setActive(requested);
110
+ }
111
+ else if (forced === AUTO_THEME_NAME) {
101
112
  applyAutoBase(name);
102
113
  setActive(AUTO_THEME_NAME);
103
114
  }
@@ -173,16 +184,41 @@ export function ThemeProvider({ children, theme, }) {
173
184
  console.warn('[dsh-tui] failed to write ~/.dsh-tui/theme.json');
174
185
  return false;
175
186
  }
187
+ requestedThemeRef.current = name;
176
188
  setActive(name);
177
189
  if (name === AUTO_THEME_NAME)
178
190
  redetectAutoBase();
179
191
  return true;
180
192
  }, [redetectAutoBase]);
181
- const value = React.useMemo(() => ({ theme: active ?? 'dark', autoBase, setTheme }), [active, autoBase, setTheme]);
193
+ useEffect(() => {
194
+ if (active !== null && !isThemeAvailable(active)) {
195
+ // A runtime plugin can disappear while its name remains persisted. Do
196
+ // not render getTheme(name)'s dark fallback under the stale name; keep
197
+ // the requested name in the ref and use a safe auto palette instead.
198
+ setActive(AUTO_THEME_NAME);
199
+ redetectAutoBase();
200
+ return;
201
+ }
202
+ const requested = requestedThemeRef.current;
203
+ if (requested !== undefined &&
204
+ requested !== AUTO_THEME_NAME &&
205
+ requested !== active &&
206
+ isThemeAvailable(requested)) {
207
+ // The plugin theme was unavailable during boot (or briefly unloaded),
208
+ // but has registered again. Restore the persisted/requested choice.
209
+ setActive(requested);
210
+ }
211
+ }, [active, redetectAutoBase, runtimeThemeSnapshot]);
212
+ const renderedTheme = active === null
213
+ ? 'dark'
214
+ : isThemeAvailable(active)
215
+ ? active
216
+ : AUTO_THEME_NAME;
217
+ const value = React.useMemo(() => ({ theme: renderedTheme, autoBase, setTheme }), [renderedTheme, autoBase, setTheme]);
182
218
  useEffect(() => {
183
219
  if (active !== null)
184
- setActiveThemeName(active);
185
- }, [active]);
220
+ setActiveThemeName(renderedTheme);
221
+ }, [active, renderedTheme]);
186
222
  if (active === null)
187
223
  return null;
188
224
  return _jsx(ThemeContext.Provider, { value: value, children: children });
@@ -1,8 +1,12 @@
1
1
  import React, { type PropsWithChildren, type Ref } from 'react';
2
2
  import type { DOMElement } from '../../ink/dom.js';
3
3
  import type { ClickEvent } from '../../ink/events/click-event.js';
4
+ import type { ContextMenuEvent } from '../../ink/events/context-menu-event.js';
5
+ import type { DragEvent } from '../../ink/events/drag-event.js';
4
6
  import type { FocusEvent } from '../../ink/events/focus-event.js';
5
7
  import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
8
+ import type { PointerEvent } from '../../ink/events/pointer-event.js';
9
+ import type { WheelEvent } from '../../ink/events/wheel-event.js';
6
10
  import type { Color, Styles } from '../../ink/styles.js';
7
11
  import { type Theme } from '../../theme.js';
8
12
  type ThemedColorProps = {
@@ -19,14 +23,31 @@ export type Props = BaseStylesWithoutColors & ThemedColorProps & {
19
23
  tabIndex?: number;
20
24
  autoFocus?: boolean;
21
25
  onClick?: (event: ClickEvent) => void;
26
+ onContextMenu?: (event: ContextMenuEvent) => void;
27
+ /**
28
+ * Drag protocol handlers, transparently forwarded to the inner Box.
29
+ * Only inside `<AlternateScreen>`, left button, no modifier at press;
30
+ * `dragstart` fires on FIRST movement (not press); a press+release
31
+ * without movement still triggers `onClick`. See Box's JSDoc.
32
+ */
33
+ onDragStart?: (event: DragEvent) => void;
34
+ /** Fired on each pointer motion after dragstart. See onDragStart. */
35
+ onDragMove?: (event: DragEvent) => void;
36
+ /** Fired on release after dragstart. See onDragStart. */
37
+ onDragEnd?: (event: DragEvent) => void;
22
38
  onFocus?: (event: FocusEvent) => void;
23
39
  onFocusCapture?: (event: FocusEvent) => void;
24
40
  onBlur?: (event: FocusEvent) => void;
25
41
  onBlurCapture?: (event: FocusEvent) => void;
26
42
  onKeyDown?: (event: KeyboardEvent) => void;
27
43
  onKeyDownCapture?: (event: KeyboardEvent) => void;
28
- onMouseEnter?: () => void;
29
- onMouseLeave?: () => void;
44
+ onMouseEnter?: (event: PointerEvent) => void;
45
+ onMouseLeave?: (event: PointerEvent) => void;
46
+ /**
47
+ * Wheel events over this Box's rendered rect (position-routed; see the
48
+ * ink Box's onWheel JSDoc). Transparently forwarded.
49
+ */
50
+ onWheel?: (event: WheelEvent) => void;
30
51
  };
31
52
  /**
32
53
  * Theme-aware Box component that resolves theme color keys to raw colors
@@ -1 +1 @@
1
- {"version":3,"file":"ThemedBox.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/ThemedBox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,iBAAiB,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAA;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAA;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EAAY,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAIrD,KAAK,gBAAgB,GAAG;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,KAAK,GAAG,KAAK,CAAA;IAC1C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,KAAK,GAAG,KAAK,CAAA;IAC7C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,KAAK,GAAG,KAAK,CAAA;IAChD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,GAAG,KAAK,CAAA;IAC9C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,KAAK,GAAG,KAAK,CAAA;IAC/C,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,KAAK,GAAG,KAAK,CAAA;CAC/C,CAAA;AAGD,KAAK,uBAAuB,GAAG,IAAI,CACjC,MAAM,EACJ,UAAU,GACV,aAAa,GACb,gBAAgB,GAChB,mBAAmB,GACnB,iBAAiB,GACjB,kBAAkB,GAClB,iBAAiB,CACpB,CAAA;AAED,MAAM,MAAM,KAAK,GAAG,uBAAuB,GACzC,gBAAgB,GAAG;IACjB,GAAG,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;IACrC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;IACrC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;IAC5C,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;IACpC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;IAC3C,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;IAC1C,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;IACjD,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;CAC1B,CAAA;AAsBH;;;GAGG;AACH,iBAAS,SAAS,CAAC,EACjB,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,GAAG,IAAI,EACR,EAAE,iBAAiB,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,SAAS,CAc5C;AACD,eAAe,SAAS,CAAA"}
1
+ {"version":3,"file":"ThemedBox.d.ts","sourceRoot":"","sources":["../../../../src/components/design-system/ThemedBox.tsx"],"names":[],"mappings":"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"}
@@ -2,9 +2,14 @@ import React from 'react';
2
2
  import type { ClickEvent } from '../../ink/events/click-event.js';
3
3
  type Props = {
4
4
  thinking: string;
5
+ /** The FULL un-revealed text (reasoning rows under smooth streaming):
6
+ * `thinking` carries the revealed slice the expanded body paints, while
7
+ * the live preview ticker must follow the newest ARRIVED content — never
8
+ * a lagging reveal. Falls back to `thinking`. */
9
+ textFull?: string;
5
10
  /** Adds the top margin between messages (CC: addMargin). */
6
11
  addMargin: boolean;
7
- /** True when Ctrl+O transcript/verbose mode is on — show the full text. */
12
+ /** Show the full text (Ctrl+O, per-row expansion, or live click toggle). */
8
13
  verbose: boolean;
9
14
  /** True while the reasoning block is still streaming — the leading anchor
10
15
  * becomes a rotating braille spinner (Kimi Code style) and settles back
@@ -21,14 +26,14 @@ type Props = {
21
26
  onClick?(event: ClickEvent): void;
22
27
  };
23
28
  /**
24
- * Thinking block: folded `⚓ Thinking (ctrl+o to expand)`, expanded shows the
25
- * full reasoning text indented under `⚓ Thinking…`. While the reasoning
26
- * streams the leading mark is a rotating braille spinner (`⠋⠙⠹…`, Kimi Code
27
- * style), settling back to the static anchor (`⚓`) once the turn ends. When
29
+ * Thinking block: settled rows fold to `⚓ Thinking (ctrl+o to expand)`;
30
+ * streaming rows switch between a three-line preview and the full reasoning
31
+ * text on click. The live leading mark is a rotating braille spinner
32
+ * (`⠋⠙⠹…`, Kimi Code style), settling back to the static anchor (`⚓`). When
28
33
  * the channel records the reasoning duration, the label carries it
29
34
  * (`⚓ Thinking · 12s …`) — dsh-tui's take on making thinking time visible in
30
35
  * the transcript.
31
36
  */
32
- export declare function AssistantThinkingMessage({ thinking, addMargin, verbose, streaming, preview, durationMs, isSelected, onClick, }: Props): React.ReactNode;
37
+ export declare function AssistantThinkingMessage({ thinking, textFull, addMargin, verbose, streaming, preview, durationMs, isSelected, onClick, }: Props): React.ReactNode;
33
38
  export {};
34
39
  //# sourceMappingURL=AssistantThinkingMessage.d.ts.map