@dungle-scrubs/tallow 0.9.3 → 0.9.6

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 (207) hide show
  1. package/dist/cli.js +7 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/interactive-mode-patch.d.ts +24 -10
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +285 -148
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/interactive-reset.d.ts +49 -0
  10. package/dist/interactive-reset.d.ts.map +1 -0
  11. package/dist/interactive-reset.js +40 -0
  12. package/dist/interactive-reset.js.map +1 -0
  13. package/dist/pi-tui-editor-patch.d.ts +10 -0
  14. package/dist/pi-tui-editor-patch.d.ts.map +1 -0
  15. package/dist/pi-tui-editor-patch.js +159 -0
  16. package/dist/pi-tui-editor-patch.js.map +1 -0
  17. package/dist/pi-tui-patch.d.ts +2 -0
  18. package/dist/pi-tui-patch.d.ts.map +1 -0
  19. package/dist/pi-tui-patch.js +563 -0
  20. package/dist/pi-tui-patch.js.map +1 -0
  21. package/dist/pi-tui-settings-list-patch.d.ts +11 -0
  22. package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
  23. package/dist/pi-tui-settings-list-patch.js +38 -0
  24. package/dist/pi-tui-settings-list-patch.js.map +1 -0
  25. package/dist/reset-diagnostics.d.ts +69 -0
  26. package/dist/reset-diagnostics.d.ts.map +1 -0
  27. package/dist/reset-diagnostics.js +41 -0
  28. package/dist/reset-diagnostics.js.map +1 -0
  29. package/dist/sdk.d.ts +5 -21
  30. package/dist/sdk.d.ts.map +1 -1
  31. package/dist/sdk.js +180 -149
  32. package/dist/sdk.js.map +1 -1
  33. package/dist/workspace-transition-interactive.d.ts +1 -0
  34. package/dist/workspace-transition-interactive.d.ts.map +1 -1
  35. package/dist/workspace-transition-interactive.js +7 -17
  36. package/dist/workspace-transition-interactive.js.map +1 -1
  37. package/extensions/__integration__/audit-findings.test.ts +6 -16
  38. package/extensions/__integration__/teams-runtime.test.ts +4 -1
  39. package/extensions/_icons/index.ts +2 -4
  40. package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
  41. package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
  42. package/extensions/_shared/image-metadata.ts +99 -0
  43. package/extensions/_shared/inline-preview.ts +1 -1
  44. package/extensions/_shared/pid-registry.ts +0 -1
  45. package/extensions/_shared/terminal-links.ts +22 -0
  46. package/extensions/ask-user-question-tool/index.ts +0 -3
  47. package/extensions/clear/__tests__/clear.test.ts +270 -3
  48. package/extensions/command-expansion/index.ts +1 -1
  49. package/extensions/context-files/index.ts +5 -1
  50. package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
  51. package/extensions/context-fork/extension.json +1 -1
  52. package/extensions/context-fork/index.ts +32 -0
  53. package/extensions/edit-tool-enhanced/index.ts +2 -1
  54. package/extensions/hooks/index.ts +33 -11
  55. package/extensions/loop/index.ts +14 -1
  56. package/extensions/lsp/index.ts +64 -13
  57. package/extensions/lsp/package.json +2 -2
  58. package/extensions/permissions/__tests__/permissions.test.ts +4 -4
  59. package/extensions/random-spinner/index.ts +7 -642
  60. package/extensions/read-tool-enhanced/index.ts +6 -8
  61. package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +4 -5
  62. package/extensions/render-stabilizer/index.ts +6 -6
  63. package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +1 -1
  64. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
  65. package/extensions/slash-command-bridge/index.ts +14 -2
  66. package/extensions/subagent-tool/index.ts +1 -1
  67. package/extensions/subagent-tool/model-resolver.ts +274 -7
  68. package/extensions/tasks/__tests__/state-ui.test.ts +3 -3
  69. package/extensions/tasks/__tests__/widget-subagents.test.ts +2 -2
  70. package/extensions/tasks/commands/register-tasks-extension.ts +10 -10
  71. package/extensions/tasks/state/index.ts +1 -1
  72. package/extensions/tasks/ui/index.ts +2 -7
  73. package/extensions/teams-tool/tools/register-extension.ts +1 -3
  74. package/extensions/web-search-tool/index.ts +2 -1
  75. package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +21 -6
  76. package/extensions/write-tool-enhanced/index.ts +2 -1
  77. package/node_modules/@mariozechner/pi-tui/README.md +56 -34
  78. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
  79. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
  80. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
  81. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
  82. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
  83. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
  84. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
  85. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
  86. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
  87. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
  88. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
  89. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
  90. package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
  91. package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
  92. package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
  93. package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
  94. package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
  95. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
  96. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
  97. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
  98. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
  99. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
  100. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
  101. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
  102. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
  103. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
  104. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
  105. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
  106. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +0 -2
  107. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
  108. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +12 -23
  109. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
  110. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
  111. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
  112. package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
  113. package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
  114. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
  115. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
  116. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
  117. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
  118. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
  119. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
  120. package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
  121. package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
  122. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
  123. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
  124. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
  125. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
  126. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
  127. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
  128. package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
  129. package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
  130. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
  131. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
  132. package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
  133. package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
  134. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
  135. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
  136. package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
  137. package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
  138. package/node_modules/@mariozechner/pi-tui/package.json +6 -6
  139. package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
  140. package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
  141. package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
  142. package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
  143. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
  144. package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
  145. package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
  146. package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
  147. package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
  148. package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
  149. package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
  150. package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
  151. package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
  152. package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
  153. package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +11 -23
  154. package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
  155. package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
  156. package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
  157. package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
  158. package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
  159. package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
  160. package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
  161. package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
  162. package/package.json +13 -13
  163. package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
  164. package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
  165. package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
  166. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
  167. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
  168. package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
  169. package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
  170. package/packages/tallow-tui/node_modules/marked/README.md +5 -4
  171. package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
  172. package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
  173. package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
  174. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
  175. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
  176. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
  177. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
  178. package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
  179. package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
  180. package/packages/tallow-tui/node_modules/marked/package.json +26 -34
  181. package/runtime/model-metadata-overrides.ts +10 -1
  182. package/runtime/pid-schema.ts +26 -6
  183. package/skills/tallow-expert/SKILL.md +1 -3
  184. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
  185. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
  186. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
  187. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
  188. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
  189. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
  190. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
  191. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
  192. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
  193. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
  194. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
  195. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
  196. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
  197. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
  198. package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
  199. package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -49
  200. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
  201. package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
  202. package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
  203. package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
  204. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
  205. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
  206. package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
  207. package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
@@ -1,141 +1,36 @@
1
1
  import type { TUI } from "../tui.js";
2
2
  import { Text } from "./text.js";
3
3
 
4
- /** Default braille spinner frames used when no custom frames are provided. */
5
- const DEFAULT_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
6
-
7
- /** Default animation interval in milliseconds. */
8
- const DEFAULT_INTERVAL_MS = 80;
9
-
10
- /** Optional configuration for the Loader's spinner appearance and timing. */
11
- export interface LoaderOptions {
12
- /** Spinner animation frames (defaults to braille dots). */
13
- frames?: string[];
14
- /** Animation interval in ms (default: 80). */
15
- intervalMs?: number;
16
- }
17
-
18
- /** Context passed to the message transform callback each tick. */
19
- export interface MessageTransformContext {
20
- /** The current message set on this Loader (from constructor or setMessage). */
21
- message: string;
22
- /** Monotonic tick counter — increments each animation frame for this instance. */
23
- tick: number;
24
- /** True if the message has not been changed via setMessage() since construction. */
25
- isInitialMessage: boolean;
26
- }
27
-
28
4
  /**
29
- * Loader component that updates with a spinning animation.
30
- *
31
- * @param ui - TUI instance for rendering
32
- * @param spinnerColorFn - Color function applied to the spinner frame
33
- * @param messageColorFn - Color function applied to the message text
34
- * @param message - Text shown next to the spinner (default: "Loading...")
35
- * @param options - Optional frames and interval override
5
+ * Loader component that updates every 80ms with spinning animation
36
6
  */
37
7
  export class Loader extends Text {
38
- private frames: string[];
39
- private intervalMs: number;
8
+ private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
40
9
  private currentFrame = 0;
41
10
  private intervalId: NodeJS.Timeout | null = null;
42
- private _transformIntervalId: NodeJS.Timeout | null = null;
43
11
  private ui: TUI | null = null;
44
12
 
45
- /** Per-instance tick counter for message transform animations. */
46
- private _transformTick = 0;
47
-
48
- /** Tracks whether setMessage() has been called since construction. */
49
- private _messageChanged = false;
50
-
51
13
  constructor(
52
14
  ui: TUI,
53
15
  private spinnerColorFn: (str: string) => string,
54
16
  private messageColorFn: (str: string) => string,
55
- private message: string = "Loading...",
56
- options?: LoaderOptions
17
+ private message: string = "Loading..."
57
18
  ) {
58
19
  super("", 1, 0);
59
- this.frames = options?.frames ?? Loader.defaultFrames ?? DEFAULT_FRAMES;
60
- this.intervalMs = options?.intervalMs ?? Loader.defaultIntervalMs ?? DEFAULT_INTERVAL_MS;
61
20
  this.ui = ui;
62
21
  this.start();
63
22
  }
64
23
 
65
- /**
66
- * Global default spinner frames — set once, applies to all new Loader instances.
67
- * Extensions can set this at session_start to override the braille default.
68
- */
69
- static defaultFrames: string[] | undefined;
70
-
71
- /**
72
- * Global default interval — set once, applies to all new Loader instances.
73
- */
74
- static defaultIntervalMs: number | undefined;
75
-
76
- /**
77
- * Global message transform — called each tick to modify the displayed message.
78
- * Extensions use this to animate or replace the loader text.
79
- * Return the string to display. The transform is applied before messageColorFn.
80
- */
81
- static defaultMessageTransform?: (ctx: MessageTransformContext) => string;
82
-
83
- /**
84
- * Interval (ms) for the message transform tick — independent of the spinner frame rate.
85
- * When set to a value faster than the spinner interval, a separate timer drives
86
- * the transform tick and re-renders, giving animations higher
87
- * frame rates without affecting the spinner animation speed.
88
- */
89
- static defaultTransformIntervalMs: number | undefined;
90
-
91
- /** Whether the loader is hidden (renders as empty space). */
92
- private hidden = false;
93
-
94
- /**
95
- * Hide the loader — renders nothing but keeps the interval alive.
96
- * Call show() to restore.
97
- */
98
- hide() {
99
- this.hidden = true;
100
- this.setText("");
101
- this.ui?.requestRender();
102
- }
103
-
104
- /**
105
- * Show the loader after a hide() call.
106
- */
107
- show() {
108
- this.hidden = false;
109
- this.updateDisplay();
110
- }
111
-
112
24
  render(width: number): string[] {
113
- if (this.hidden) return [];
114
25
  return ["", ...super.render(width)];
115
26
  }
116
27
 
117
28
  start() {
118
29
  this.updateDisplay();
119
-
120
- const transformMs = Loader.defaultTransformIntervalMs;
121
- const hasFastTransform =
122
- Loader.defaultMessageTransform != null &&
123
- transformMs != null &&
124
- transformMs < this.intervalMs;
125
-
126
30
  this.intervalId = setInterval(() => {
127
31
  this.currentFrame = (this.currentFrame + 1) % this.frames.length;
128
- this._transformTick++;
129
32
  this.updateDisplay();
130
- }, this.intervalMs);
131
-
132
- // Separate faster interval for message transform re-renders only.
133
- // Does NOT advance _transformTick — just re-rolls random visuals.
134
- if (hasFastTransform) {
135
- this._transformIntervalId = setInterval(() => {
136
- this.updateDisplay();
137
- }, transformMs);
138
- }
33
+ }, 80);
139
34
  }
140
35
 
141
36
  stop() {
@@ -143,43 +38,16 @@ export class Loader extends Text {
143
38
  clearInterval(this.intervalId);
144
39
  this.intervalId = null;
145
40
  }
146
- if (this._transformIntervalId) {
147
- clearInterval(this._transformIntervalId);
148
- this._transformIntervalId = null;
149
- }
150
41
  }
151
42
 
152
- /** Sentinel value — pass to setWorkingMessage() to hide the loader. */
153
- static readonly HIDE = "\u200B";
154
-
155
- /**
156
- * Set the loader message. Pass Loader.HIDE to hide, any other string to show.
157
- * @param message - Message text or Loader.HIDE sentinel
158
- */
159
43
  setMessage(message: string) {
160
- if (message === Loader.HIDE) {
161
- this.hide();
162
- return;
163
- }
164
- if (this.hidden) this.show();
165
- this._messageChanged = true;
166
44
  this.message = message;
167
45
  this.updateDisplay();
168
46
  }
169
47
 
170
48
  private updateDisplay() {
171
49
  const frame = this.frames[this.currentFrame];
172
- let displayMessage = this.message;
173
-
174
- if (Loader.defaultMessageTransform) {
175
- displayMessage = Loader.defaultMessageTransform({
176
- message: this.message,
177
- tick: this._transformTick,
178
- isInitialMessage: !this._messageChanged,
179
- });
180
- }
181
-
182
- this.setText(`${this.spinnerColorFn(frame)} ${this.messageColorFn(displayMessage)}`);
50
+ this.setText(`${this.spinnerColorFn(frame)} ${this.messageColorFn(this.message)}`);
183
51
  if (this.ui) {
184
52
  this.ui.requestRender();
185
53
  }
@@ -1,8 +1,32 @@
1
- import { marked, type Token } from "marked";
2
- import { isImageLine } from "../terminal-image.js";
1
+ import { Marked, type Token, Tokenizer, type Tokens } from "marked";
2
+ import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js";
3
3
  import type { Component } from "../tui.js";
4
4
  import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
5
5
 
6
+ const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
7
+
8
+ class StrictStrikethroughTokenizer extends Tokenizer {
9
+ override del(src: string): Tokens.Del | undefined {
10
+ const match = STRICT_STRIKETHROUGH_REGEX.exec(src);
11
+ if (!match) {
12
+ return undefined;
13
+ }
14
+
15
+ const text = match[2];
16
+ return {
17
+ type: "del",
18
+ raw: match[0],
19
+ text,
20
+ tokens: this.lexer.inlineTokens(text),
21
+ };
22
+ }
23
+ }
24
+
25
+ const markdownParser = new Marked();
26
+ markdownParser.setOptions({
27
+ tokenizer: new StrictStrikethroughTokenizer(),
28
+ });
29
+
6
30
  /**
7
31
  * Default text styling for markdown content.
8
32
  * Applied to all text unless overridden by markdown formatting.
@@ -112,7 +136,7 @@ export class Markdown implements Component {
112
136
  const normalizedText = this.text.replace(/\t/g, " ");
113
137
 
114
138
  // Parse markdown to HTML-like tokens
115
- const tokens = marked.lexer(normalizedText);
139
+ const tokens = markdownParser.lexer(normalizedText);
116
140
 
117
141
  // Convert tokens to styled terminal output
118
142
  const renderedLines: string[] = [];
@@ -260,31 +284,47 @@ export class Markdown implements Component {
260
284
  };
261
285
  }
262
286
 
263
- private renderToken(token: Token, width: number, nextTokenType?: string): string[] {
287
+ private renderToken(
288
+ token: Token,
289
+ width: number,
290
+ nextTokenType?: string,
291
+ styleContext?: InlineStyleContext
292
+ ): string[] {
264
293
  const lines: string[] = [];
265
294
 
266
295
  switch (token.type) {
267
296
  case "heading": {
268
297
  const headingLevel = token.depth;
269
298
  const headingPrefix = `${"#".repeat(headingLevel)} `;
270
- const headingText = this.renderInlineTokens(token.tokens || []);
271
- let styledHeading: string;
299
+
300
+ // Build a heading-specific style context so inline tokens (codespan, bold, etc.)
301
+ // restore heading styling after their own ANSI resets instead of falling back to
302
+ // the default text style.
303
+ let headingStyleFn: (text: string) => string;
272
304
  if (headingLevel === 1) {
273
- styledHeading = this.theme.heading(this.theme.bold(this.theme.underline(headingText)));
274
- } else if (headingLevel === 2) {
275
- styledHeading = this.theme.heading(this.theme.bold(headingText));
305
+ headingStyleFn = (text: string) =>
306
+ this.theme.heading(this.theme.bold(this.theme.underline(text)));
276
307
  } else {
277
- styledHeading = this.theme.heading(this.theme.bold(headingPrefix + headingText));
308
+ headingStyleFn = (text: string) => this.theme.heading(this.theme.bold(text));
278
309
  }
310
+
311
+ const headingStyleContext: InlineStyleContext = {
312
+ applyText: headingStyleFn,
313
+ stylePrefix: this.getStylePrefix(headingStyleFn),
314
+ };
315
+
316
+ const headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext);
317
+ const styledHeading =
318
+ headingLevel >= 3 ? headingStyleFn(headingPrefix) + headingText : headingText;
279
319
  lines.push(styledHeading);
280
- if (nextTokenType !== "space") {
320
+ if (nextTokenType && nextTokenType !== "space") {
281
321
  lines.push(""); // Add spacing after headings (unless space token follows)
282
322
  }
283
323
  break;
284
324
  }
285
325
 
286
326
  case "paragraph": {
287
- const paragraphText = this.renderInlineTokens(token.tokens || []);
327
+ const paragraphText = this.renderInlineTokens(token.tokens || [], styleContext);
288
328
  lines.push(paragraphText);
289
329
  // Don't add spacing if next token is space or list
290
330
  if (nextTokenType && nextTokenType !== "list" && nextTokenType !== "space") {
@@ -309,14 +349,14 @@ export class Markdown implements Component {
309
349
  }
310
350
  }
311
351
  lines.push(this.theme.codeBlockBorder("```"));
312
- if (nextTokenType !== "space") {
352
+ if (nextTokenType && nextTokenType !== "space") {
313
353
  lines.push(""); // Add spacing after code blocks (unless space token follows)
314
354
  }
315
355
  break;
316
356
  }
317
357
 
318
358
  case "list": {
319
- const listLines = this.renderList(token as any, 0);
359
+ const listLines = this.renderList(token as any, 0, styleContext);
320
360
  lines.push(...listLines);
321
361
  // Don't add spacing after lists if a space token follows
322
362
  // (the space token will handle it)
@@ -324,31 +364,63 @@ export class Markdown implements Component {
324
364
  }
325
365
 
326
366
  case "table": {
327
- const tableLines = this.renderTable(token as any, width);
367
+ const tableLines = this.renderTable(token as any, width, nextTokenType, styleContext);
328
368
  lines.push(...tableLines);
329
369
  break;
330
370
  }
331
371
 
332
372
  case "blockquote": {
333
373
  const quoteStyle = (text: string) => this.theme.quote(this.theme.italic(text));
334
- const quoteStyleContext: InlineStyleContext = {
335
- applyText: quoteStyle,
336
- stylePrefix: this.getStylePrefix(quoteStyle),
374
+ const quoteStylePrefix = this.getStylePrefix(quoteStyle);
375
+ const applyQuoteStyle = (line: string): string => {
376
+ if (!quoteStylePrefix) {
377
+ return quoteStyle(line);
378
+ }
379
+ const lineWithReappliedStyle = line.replace(/\x1b\[0m/g, `\x1b[0m${quoteStylePrefix}`);
380
+ return quoteStyle(lineWithReappliedStyle);
337
381
  };
338
- const quoteText = this.renderInlineTokens(token.tokens || [], quoteStyleContext);
339
- const quoteLines = quoteText.split("\n");
340
382
 
341
383
  // Calculate available width for quote content (subtract border "│ " = 2 chars)
342
384
  const quoteContentWidth = Math.max(1, width - 2);
343
385
 
344
- for (const quoteLine of quoteLines) {
345
- // Wrap the styled line, then add border to each wrapped line
346
- const wrappedLines = wrapTextWithAnsi(quoteLine, quoteContentWidth);
386
+ // Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render
387
+ // children with renderToken() instead of renderInlineTokens().
388
+ // Default message style should not apply inside blockquotes.
389
+ const quoteInlineStyleContext: InlineStyleContext = {
390
+ applyText: (text: string) => text,
391
+ stylePrefix: quoteStylePrefix,
392
+ };
393
+ const quoteTokens = token.tokens || [];
394
+ const renderedQuoteLines: string[] = [];
395
+ for (let i = 0; i < quoteTokens.length; i++) {
396
+ const quoteToken = quoteTokens[i];
397
+ const nextQuoteToken = quoteTokens[i + 1];
398
+ renderedQuoteLines.push(
399
+ ...this.renderToken(
400
+ quoteToken,
401
+ quoteContentWidth,
402
+ nextQuoteToken?.type,
403
+ quoteInlineStyleContext
404
+ )
405
+ );
406
+ }
407
+
408
+ // Avoid rendering an extra empty quote line before the outer blockquote spacing.
409
+ while (
410
+ renderedQuoteLines.length > 0 &&
411
+ renderedQuoteLines[renderedQuoteLines.length - 1] === ""
412
+ ) {
413
+ renderedQuoteLines.pop();
414
+ }
415
+
416
+ for (const quoteLine of renderedQuoteLines) {
417
+ const styledLine = applyQuoteStyle(quoteLine);
418
+ const wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth);
347
419
  for (const wrappedLine of wrappedLines) {
348
420
  lines.push(this.theme.quoteBorder("│ ") + wrappedLine);
349
421
  }
350
422
  }
351
- if (nextTokenType !== "space") {
423
+ if (nextTokenType && nextTokenType !== "space") {
352
424
  lines.push(""); // Add spacing after blockquotes (unless space token follows)
353
425
  }
354
426
  break;
@@ -356,7 +428,7 @@ export class Markdown implements Component {
356
428
 
357
429
  case "hr":
358
430
  lines.push(this.theme.hr("─".repeat(Math.min(width, 80))));
359
- if (nextTokenType !== "space") {
431
+ if (nextTokenType && nextTokenType !== "space") {
360
432
  lines.push(""); // Add spacing after horizontal rules (unless space token follows)
361
433
  }
362
434
  break;
@@ -426,20 +498,24 @@ export class Markdown implements Component {
426
498
 
427
499
  case "link": {
428
500
  const linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
429
- // If link text matches href, only show the link once
430
- // Compare raw text (token.text) not styled text (linkText) since linkText has ANSI codes
431
- // For mailto: links, strip the prefix before comparing (autolinked emails have
432
- // text="foo@bar.com" but href="mailto:foo@bar.com")
433
- const hrefForComparison = token.href.startsWith("mailto:")
434
- ? token.href.slice(7)
435
- : token.href;
436
- if (token.text === token.href || token.text === hrefForComparison) {
437
- result += this.theme.link(this.theme.underline(linkText)) + stylePrefix;
501
+ const styledLink = this.theme.link(this.theme.underline(linkText));
502
+ if (getCapabilities().hyperlinks) {
503
+ // OSC 8: render as a clickable hyperlink. The URL is not printed inline,
504
+ // so we always show only the link text regardless of whether it matches href.
505
+ result += hyperlink(styledLink, token.href) + stylePrefix;
438
506
  } else {
439
- result +=
440
- this.theme.link(this.theme.underline(linkText)) +
441
- this.theme.linkUrl(` (${token.href})`) +
442
- stylePrefix;
507
+ // Fallback: print URL in parentheses when text differs from href.
508
+ // Compare raw token.text (not styled) against href for the equality check.
509
+ // For mailto: links strip the prefix (autolinked emails use text="foo@bar.com"
510
+ // but href="mailto:foo@bar.com").
511
+ const hrefForComparison = token.href.startsWith("mailto:")
512
+ ? token.href.slice(7)
513
+ : token.href;
514
+ if (token.text === token.href || token.text === hrefForComparison) {
515
+ result += styledLink + stylePrefix;
516
+ } else {
517
+ result += styledLink + this.theme.linkUrl(` (${token.href})`) + stylePrefix;
518
+ }
443
519
  }
444
520
  break;
445
521
  }
@@ -469,6 +545,10 @@ export class Markdown implements Component {
469
545
  }
470
546
  }
471
547
 
548
+ while (stylePrefix && result.endsWith(stylePrefix)) {
549
+ result = result.slice(0, -stylePrefix.length);
550
+ }
551
+
472
552
  return result;
473
553
  }
474
554
 
@@ -477,7 +557,8 @@ export class Markdown implements Component {
477
557
  */
478
558
  private renderList(
479
559
  token: Token & { items: any[]; ordered: boolean; start?: number },
480
- depth: number
560
+ depth: number,
561
+ styleContext?: InlineStyleContext
481
562
  ): string[] {
482
563
  const lines: string[] = [];
483
564
  const indent = " ".repeat(depth);
@@ -489,7 +570,7 @@ export class Markdown implements Component {
489
570
  const bullet = token.ordered ? `${startNumber + i}. ` : "- ";
490
571
 
491
572
  // Process item tokens to handle nested lists
492
- const itemLines = this.renderListItem(item.tokens || [], depth);
573
+ const itemLines = this.renderListItem(item.tokens || [], depth, styleContext);
493
574
 
494
575
  if (itemLines.length > 0) {
495
576
  // First line - check if it's a nested list
@@ -530,25 +611,29 @@ export class Markdown implements Component {
530
611
  * Render list item tokens, handling nested lists
531
612
  * Returns lines WITHOUT the parent indent (renderList will add it)
532
613
  */
533
- private renderListItem(tokens: Token[], parentDepth: number): string[] {
614
+ private renderListItem(
615
+ tokens: Token[],
616
+ parentDepth: number,
617
+ styleContext?: InlineStyleContext
618
+ ): string[] {
534
619
  const lines: string[] = [];
535
620
 
536
621
  for (const token of tokens) {
537
622
  if (token.type === "list") {
538
623
  // Nested list - render with one additional indent level
539
624
  // These lines will have their own indent, so we just add them as-is
540
- const nestedLines = this.renderList(token as any, parentDepth + 1);
625
+ const nestedLines = this.renderList(token as any, parentDepth + 1, styleContext);
541
626
  lines.push(...nestedLines);
542
627
  } else if (token.type === "text") {
543
628
  // Text content (may have inline tokens)
544
629
  const text =
545
630
  token.tokens && token.tokens.length > 0
546
- ? this.renderInlineTokens(token.tokens)
631
+ ? this.renderInlineTokens(token.tokens, styleContext)
547
632
  : token.text || "";
548
633
  lines.push(text);
549
634
  } else if (token.type === "paragraph") {
550
635
  // Paragraph in list item
551
- const text = this.renderInlineTokens(token.tokens || []);
636
+ const text = this.renderInlineTokens(token.tokens || [], styleContext);
552
637
  lines.push(text);
553
638
  } else if (token.type === "code") {
554
639
  // Code block in list item
@@ -568,7 +653,7 @@ export class Markdown implements Component {
568
653
  lines.push(this.theme.codeBlockBorder("```"));
569
654
  } else {
570
655
  // Other token types - try to render as inline
571
- const text = this.renderInlineTokens([token]);
656
+ const text = this.renderInlineTokens([token], styleContext);
572
657
  if (text) {
573
658
  lines.push(text);
574
659
  }
@@ -609,7 +694,9 @@ export class Markdown implements Component {
609
694
  */
610
695
  private renderTable(
611
696
  token: Token & { header: any[]; rows: any[][]; raw?: string },
612
- availableWidth: number
697
+ availableWidth: number,
698
+ nextTokenType?: string,
699
+ styleContext?: InlineStyleContext
613
700
  ): string[] {
614
701
  const lines: string[] = [];
615
702
  const numCols = token.header.length;
@@ -625,7 +712,9 @@ export class Markdown implements Component {
625
712
  if (availableForCells < numCols) {
626
713
  // Too narrow to render a stable table. Fall back to raw markdown.
627
714
  const fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];
628
- fallbackLines.push("");
715
+ if (nextTokenType && nextTokenType !== "space") {
716
+ fallbackLines.push("");
717
+ }
629
718
  return fallbackLines;
630
719
  }
631
720
 
@@ -635,13 +724,13 @@ export class Markdown implements Component {
635
724
  const naturalWidths: number[] = [];
636
725
  const minWordWidths: number[] = [];
637
726
  for (let i = 0; i < numCols; i++) {
638
- const headerText = this.renderInlineTokens(token.header[i].tokens || []);
727
+ const headerText = this.renderInlineTokens(token.header[i].tokens || [], styleContext);
639
728
  naturalWidths[i] = visibleWidth(headerText);
640
729
  minWordWidths[i] = Math.max(1, this.getLongestWordWidth(headerText, maxUnbrokenWordWidth));
641
730
  }
642
731
  for (const row of token.rows) {
643
732
  for (let i = 0; i < row.length; i++) {
644
- const cellText = this.renderInlineTokens(row[i].tokens || []);
733
+ const cellText = this.renderInlineTokens(row[i].tokens || [], styleContext);
645
734
  naturalWidths[i] = Math.max(naturalWidths[i] || 0, visibleWidth(cellText));
646
735
  minWordWidths[i] = Math.max(
647
736
  minWordWidths[i] || 1,
@@ -729,7 +818,7 @@ export class Markdown implements Component {
729
818
 
730
819
  // Render header with wrapping
731
820
  const headerCellLines: string[][] = token.header.map((cell, i) => {
732
- const text = this.renderInlineTokens(cell.tokens || []);
821
+ const text = this.renderInlineTokens(cell.tokens || [], styleContext);
733
822
  return this.wrapCellText(text, columnWidths[i]);
734
823
  });
735
824
  const headerLineCount = Math.max(...headerCellLines.map((c) => c.length));
@@ -752,7 +841,7 @@ export class Markdown implements Component {
752
841
  for (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {
753
842
  const row = token.rows[rowIndex];
754
843
  const rowCellLines: string[][] = row.map((cell, i) => {
755
- const text = this.renderInlineTokens(cell.tokens || []);
844
+ const text = this.renderInlineTokens(cell.tokens || [], styleContext);
756
845
  return this.wrapCellText(text, columnWidths[i]);
757
846
  });
758
847
  const rowLineCount = Math.max(...rowCellLines.map((c) => c.length));
@@ -774,7 +863,9 @@ export class Markdown implements Component {
774
863
  const bottomBorderCells = columnWidths.map((w) => "─".repeat(w));
775
864
  lines.push(`└─${bottomBorderCells.join("─┴─")}─┘`);
776
865
 
777
- lines.push(""); // Add spacing after table
866
+ if (nextTokenType && nextTokenType !== "space") {
867
+ lines.push(""); // Add spacing after table
868
+ }
778
869
  return lines;
779
870
  }
780
871
  }