lsd-pi 1.1.4 → 1.1.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 (175) hide show
  1. package/README.md +2 -1
  2. package/dist/headless-ui.js +2 -0
  3. package/dist/onboarding.js +11 -8
  4. package/dist/resources/extensions/async-jobs/async-bash-tool.js +14 -0
  5. package/dist/resources/extensions/async-jobs/await-tool.js +14 -0
  6. package/dist/resources/extensions/async-jobs/cancel-job-tool.js +7 -0
  7. package/dist/resources/extensions/cache-timer/index.js +5 -0
  8. package/dist/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  9. package/dist/resources/extensions/codex-rotate/README.md +9 -3
  10. package/dist/resources/extensions/codex-rotate/commands.js +15 -8
  11. package/dist/resources/extensions/codex-rotate/index.js +17 -8
  12. package/dist/resources/extensions/memory/auto-extract.js +196 -80
  13. package/dist/resources/extensions/memory/dream.js +86 -19
  14. package/dist/resources/extensions/shared/rtk.js +89 -87
  15. package/dist/resources/extensions/subagent/index.js +33 -7
  16. package/dist/startup-model-validation.js +12 -2
  17. package/dist/update-check.js +2 -2
  18. package/dist/update-cmd.js +3 -3
  19. package/dist/welcome-screen.js +43 -14
  20. package/package.json +3 -2
  21. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts +2 -0
  22. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.d.ts.map +1 -0
  23. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js +46 -0
  24. package/packages/pi-coding-agent/dist/core/agent-session.clear-queue.test.js.map +1 -0
  25. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +8 -0
  26. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/core/agent-session.js +43 -4
  28. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  29. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +3 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  31. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  32. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  33. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  35. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts +48 -0
  37. package/packages/pi-coding-agent/dist/core/pty-executor.d.ts.map +1 -0
  38. package/packages/pi-coding-agent/dist/core/pty-executor.js +173 -0
  39. package/packages/pi-coding-agent/dist/core/pty-executor.js.map +1 -0
  40. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/sdk.js +16 -3
  42. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  44. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/settings-manager.js +18 -0
  46. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/tool-approval.d.ts.map +1 -1
  48. package/packages/pi-coding-agent/dist/core/tool-approval.js +2 -2
  49. package/packages/pi-coding-agent/dist/core/tool-approval.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +7 -0
  51. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/tools/index.js +23 -2
  53. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +50 -0
  55. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -0
  56. package/packages/pi-coding-agent/dist/core/tools/pty.js +289 -0
  57. package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +36 -22
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +3 -5
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +23 -62
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js +1 -4
  68. package/packages/pi-coding-agent/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js +1 -4
  71. package/packages/pi-coding-agent/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts +39 -0
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +182 -0
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +6 -0
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +36 -0
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +2 -4
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -2
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +106 -77
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +2 -5
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +4 -13
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +11 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +49 -13
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +2 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +3 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +27 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +251 -39
  107. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +2 -2
  109. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts +10 -0
  111. package/packages/pi-coding-agent/dist/utils/terminal-screen.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/utils/terminal-screen.js +67 -0
  113. package/packages/pi-coding-agent/dist/utils/terminal-screen.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts +7 -0
  115. package/packages/pi-coding-agent/dist/utils/terminal-serializer.d.ts.map +1 -0
  116. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js +67 -0
  117. package/packages/pi-coding-agent/dist/utils/terminal-serializer.js.map +1 -0
  118. package/packages/pi-coding-agent/package.json +9 -4
  119. package/packages/pi-coding-agent/src/core/agent-session.clear-queue.test.ts +50 -0
  120. package/packages/pi-coding-agent/src/core/agent-session.ts +50 -4
  121. package/packages/pi-coding-agent/src/core/extensions/types.ts +1 -1
  122. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  123. package/packages/pi-coding-agent/src/core/pty-executor.ts +229 -0
  124. package/packages/pi-coding-agent/src/core/sdk.ts +16 -3
  125. package/packages/pi-coding-agent/src/core/settings-manager.ts +27 -0
  126. package/packages/pi-coding-agent/src/core/tool-approval.ts +2 -2
  127. package/packages/pi-coding-agent/src/core/tools/index.ts +35 -2
  128. package/packages/pi-coding-agent/src/core/tools/pty.ts +354 -0
  129. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +37 -24
  130. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +22 -70
  131. package/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +1 -3
  132. package/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +1 -3
  133. package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +224 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +45 -0
  135. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +2 -3
  136. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +104 -81
  137. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +5 -19
  138. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +55 -13
  139. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
  140. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +2 -0
  141. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +3 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +296 -48
  143. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +2 -2
  144. package/packages/pi-coding-agent/src/utils/terminal-screen.ts +77 -0
  145. package/packages/pi-coding-agent/src/utils/terminal-serializer.ts +72 -0
  146. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts +2 -0
  147. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.d.ts.map +1 -0
  148. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js +105 -0
  149. package/packages/pi-tui/dist/components/__tests__/editor-dropped-image.test.js.map +1 -0
  150. package/packages/pi-tui/dist/components/editor.d.ts +4 -0
  151. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  152. package/packages/pi-tui/dist/components/editor.js +57 -3
  153. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  154. package/packages/pi-tui/dist/components/loader.d.ts +26 -6
  155. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  156. package/packages/pi-tui/dist/components/loader.js +178 -18
  157. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  158. package/packages/pi-tui/src/components/editor.ts +65 -3
  159. package/packages/pi-tui/src/components/loader.ts +196 -19
  160. package/pkg/dist/modes/interactive/theme/themes.js +2 -2
  161. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  162. package/pkg/package.json +1 -1
  163. package/src/resources/extensions/async-jobs/async-bash-tool.ts +13 -0
  164. package/src/resources/extensions/async-jobs/await-tool.ts +13 -0
  165. package/src/resources/extensions/async-jobs/cancel-job-tool.ts +8 -0
  166. package/src/resources/extensions/cache-timer/index.ts +102 -96
  167. package/src/resources/extensions/codex-rotate/IMPLEMENTATION.md +18 -13
  168. package/src/resources/extensions/codex-rotate/README.md +9 -3
  169. package/src/resources/extensions/codex-rotate/commands.ts +335 -329
  170. package/src/resources/extensions/codex-rotate/index.ts +85 -75
  171. package/src/resources/extensions/memory/auto-extract.ts +330 -204
  172. package/src/resources/extensions/memory/dream.ts +88 -21
  173. package/src/resources/extensions/memory/tests/auto-extract.test.ts +200 -144
  174. package/src/resources/extensions/shared/rtk.js +112 -0
  175. package/src/resources/extensions/subagent/index.ts +35 -6
@@ -1,22 +1,42 @@
1
1
  import type { TUI } from "../tui.js";
2
2
  import { Text } from "./text.js";
3
3
  /**
4
- * Loader component that updates every 80ms with spinning animation
4
+ * Loader component braille spinner + animated word cycling.
5
+ *
6
+ * Each word types in character by character (typewriter), holds with a moving
7
+ * blue→cyan shimmer gradient, then erases right-to-left before the next word.
5
8
  */
6
9
  export declare class Loader extends Text {
7
10
  private spinnerColorFn;
8
- private messageColorFn;
9
- private message;
11
+ private _messageColorFn;
10
12
  private frames;
11
13
  private currentFrame;
12
- private intervalId;
14
+ private spinnerIntervalId;
13
15
  private ui;
14
- constructor(ui: TUI, spinnerColorFn: (str: string) => string, messageColorFn: (str: string) => string, message?: string);
16
+ private message;
17
+ private cycleMessages;
18
+ private cycleIndex;
19
+ private phase;
20
+ private visibleChars;
21
+ private wordAnimId;
22
+ private readonly gradStart;
23
+ private readonly gradEnd;
24
+ private shimmerTick;
25
+ private shimmerIntervalId;
26
+ constructor(ui: TUI, spinnerColorFn: (str: string) => string, _messageColorFn: (str: string) => string, message?: string);
15
27
  render(width: number): string[];
28
+ setCycleMessages(messages: string[], _intervalMs?: number): void;
29
+ clearCycleMessages(): void;
30
+ setMessage(message: string): void;
31
+ resumeCycle(): void;
16
32
  start(): void;
17
33
  stop(): void;
18
34
  dispose(): void;
19
- setMessage(message: string): void;
35
+ private startWordAnimation;
36
+ private stopWordAnimation;
37
+ private scheduleWordTick;
38
+ private currentWord;
39
+ private renderAnimatedWord;
20
40
  private updateDisplay;
21
41
  }
22
42
  //# sourceMappingURL=loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/components/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;GAEG;AACH,qBAAa,MAAO,SAAQ,IAAI;IAQ9B,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,OAAO;IAThB,OAAO,CAAC,MAAM,CAAsD;IACpE,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,EAAE,CAAoB;gBAG7B,EAAE,EAAE,GAAG,EACC,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EACvC,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EACvC,OAAO,GAAE,MAAqB;IAOvC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAI/B,KAAK;IAWL,IAAI;IAOJ,OAAO;IAKP,UAAU,CAAC,OAAO,EAAE,MAAM;IAK1B,OAAO,CAAC,aAAa;CAOrB"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/components/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAqCjC;;;;;GAKG;AACH,qBAAa,MAAO,SAAQ,IAAI;IA2B9B,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,eAAe;IA1BxB,OAAO,CAAC,MAAM,CAAsD;IACpE,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,EAAE,CAAoB;IAG9B,OAAO,CAAC,OAAO,CAAc;IAG7B,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAA+B;IAGjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6C;IACvE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+C;IAGvE,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,iBAAiB,CAA+B;gBAGvD,EAAE,EAAE,GAAG,EACC,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EACvC,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EAChD,OAAO,GAAE,MAAmB;IAQ7B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAM/B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,WAAW,SAAO;IAUvD,kBAAkB;IAKlB,UAAU,CAAC,OAAO,EAAE,MAAM;IAS1B,WAAW;IAMX,KAAK;IAqBL,IAAI;IAMJ,OAAO;IAOP,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,kBAAkB;IAyB1B,OAAO,CAAC,aAAa;CASrB"}
@@ -1,53 +1,213 @@
1
1
  import { Text } from "./text.js";
2
+ // ─── ANSI helpers ─────────────────────────────────────────────────────────────
3
+ const ESC = "\x1b[";
4
+ const RESET = "\x1b[0m";
5
+ const DIM = "\x1b[2m";
6
+ const BOLD = "\x1b[1m";
7
+ /** 24-bit foreground color */
8
+ function rgb(r, g, b, s) {
9
+ return `${ESC}38;2;${r};${g};${b}m${s}${RESET}`;
10
+ }
11
+ /** Interpolate between two RGB colors at position t ∈ [0,1] */
12
+ function lerpColor(r1, g1, b1, r2, g2, b2, t) {
13
+ return [
14
+ Math.round(r1 + (r2 - r1) * t),
15
+ Math.round(g1 + (g2 - g1) * t),
16
+ Math.round(b1 + (b2 - b1) * t),
17
+ ];
18
+ }
19
+ // ─── Timing constants ─────────────────────────────────────────────────────────
20
+ const TYPING_SPEED = 55; // ms per character typed
21
+ const HOLD_TIME = 5000; // ms to hold the full word
22
+ const ERASE_SPEED = 35; // ms per character erased
2
23
  /**
3
- * Loader component that updates every 80ms with spinning animation
24
+ * Loader component braille spinner + animated word cycling.
25
+ *
26
+ * Each word types in character by character (typewriter), holds with a moving
27
+ * blue→cyan shimmer gradient, then erases right-to-left before the next word.
4
28
  */
5
29
  export class Loader extends Text {
6
- constructor(ui, spinnerColorFn, messageColorFn, message = "Loading...") {
30
+ constructor(ui, spinnerColorFn, _messageColorFn, message = "Loading") {
7
31
  super("", 1, 0);
8
32
  this.spinnerColorFn = spinnerColorFn;
9
- this.messageColorFn = messageColorFn;
10
- this.message = message;
33
+ this._messageColorFn = _messageColorFn;
34
+ // ── spinner ───────────────────────────────────────────────────────────────
11
35
  this.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
12
36
  this.currentFrame = 0;
13
- this.intervalId = null;
37
+ this.spinnerIntervalId = null;
14
38
  this.ui = null;
39
+ // ── message state ─────────────────────────────────────────────────────────
40
+ this.message = "";
41
+ // ── word animation ────────────────────────────────────────────────────────
42
+ this.cycleMessages = null;
43
+ this.cycleIndex = 0;
44
+ this.phase = "typing";
45
+ this.visibleChars = 0;
46
+ this.wordAnimId = null;
47
+ // ── shimmer gradient: theme blueHigh → blueXhigh ─────────────────────────
48
+ this.gradStart = [147, 197, 253]; // #93c5fd (blue-300)
49
+ this.gradEnd = [191, 219, 254]; // #bfdbfe (blue-200)
50
+ // Shimmer ticks independently at ~40ms for smooth gradient movement
51
+ this.shimmerTick = 0;
52
+ this.shimmerIntervalId = null;
15
53
  this.ui = ui;
54
+ this.message = message;
16
55
  this.start();
17
56
  }
18
57
  render(width) {
19
58
  return ["", ...super.render(width)];
20
59
  }
21
- start() {
22
- if (this.intervalId) {
23
- clearInterval(this.intervalId);
60
+ // ── public API ────────────────────────────────────────────────────────────
61
+ setCycleMessages(messages, _intervalMs = 3000) {
62
+ this.cycleMessages = [...messages];
63
+ this.cycleIndex = 0;
64
+ this.message = this.cycleMessages[0];
65
+ if (this.spinnerIntervalId) {
66
+ this.startWordAnimation();
24
67
  }
25
68
  this.updateDisplay();
26
- this.intervalId = setInterval(() => {
69
+ }
70
+ clearCycleMessages() {
71
+ this.stopWordAnimation();
72
+ this.cycleMessages = null;
73
+ }
74
+ setMessage(message) {
75
+ // When an explicit message is set externally (e.g. "Waiting for approval…"),
76
+ // pause the cycle so it doesn't immediately overwrite the override.
77
+ this.stopWordAnimation();
78
+ this.cycleMessages = null;
79
+ this.message = message;
80
+ this.updateDisplay();
81
+ }
82
+ resumeCycle() {
83
+ if (this.cycleMessages && !this.wordAnimId) {
84
+ this.startWordAnimation();
85
+ }
86
+ }
87
+ start() {
88
+ if (this.spinnerIntervalId)
89
+ clearInterval(this.spinnerIntervalId);
90
+ this.spinnerIntervalId = setInterval(() => {
27
91
  this.currentFrame = (this.currentFrame + 1) % this.frames.length;
28
92
  this.updateDisplay();
29
93
  }, 80);
94
+ // Shimmer ticks independently at ~40ms for smooth gradient movement
95
+ if (this.shimmerIntervalId)
96
+ clearInterval(this.shimmerIntervalId);
97
+ this.shimmerIntervalId = setInterval(() => {
98
+ this.shimmerTick++;
99
+ if (this.cycleMessages)
100
+ this.updateDisplay();
101
+ }, 40);
102
+ if (this.cycleMessages) {
103
+ this.startWordAnimation();
104
+ }
105
+ else {
106
+ this.updateDisplay();
107
+ }
30
108
  }
31
109
  stop() {
32
- if (this.intervalId) {
33
- clearInterval(this.intervalId);
34
- this.intervalId = null;
110
+ if (this.spinnerIntervalId) {
111
+ clearInterval(this.spinnerIntervalId);
112
+ this.spinnerIntervalId = null;
35
113
  }
114
+ if (this.shimmerIntervalId) {
115
+ clearInterval(this.shimmerIntervalId);
116
+ this.shimmerIntervalId = null;
117
+ }
118
+ this.stopWordAnimation();
36
119
  }
37
120
  dispose() {
38
121
  this.stop();
39
122
  this.ui = null;
40
123
  }
41
- setMessage(message) {
42
- this.message = message;
43
- this.updateDisplay();
124
+ // ── word animation internals ──────────────────────────────────────────────
125
+ startWordAnimation() {
126
+ this.stopWordAnimation();
127
+ this.phase = "typing";
128
+ this.visibleChars = 0;
129
+ this.scheduleWordTick();
130
+ }
131
+ stopWordAnimation() {
132
+ if (this.wordAnimId) {
133
+ clearTimeout(this.wordAnimId);
134
+ this.wordAnimId = null;
135
+ }
136
+ }
137
+ scheduleWordTick() {
138
+ const word = this.currentWord();
139
+ if (this.phase === "typing") {
140
+ if (this.visibleChars < word.length) {
141
+ this.wordAnimId = setTimeout(() => {
142
+ this.visibleChars++;
143
+ this.updateDisplay();
144
+ this.scheduleWordTick();
145
+ }, TYPING_SPEED);
146
+ }
147
+ else {
148
+ // Full word shown → hold
149
+ this.phase = "hold";
150
+ this.wordAnimId = setTimeout(() => {
151
+ this.phase = "erase";
152
+ this.scheduleWordTick();
153
+ }, HOLD_TIME);
154
+ }
155
+ }
156
+ else if (this.phase === "erase") {
157
+ if (this.visibleChars > 0) {
158
+ this.wordAnimId = setTimeout(() => {
159
+ this.visibleChars--;
160
+ this.updateDisplay();
161
+ this.scheduleWordTick();
162
+ }, ERASE_SPEED);
163
+ }
164
+ else {
165
+ // Advance to next word
166
+ if (this.cycleMessages) {
167
+ this.cycleIndex = (this.cycleIndex + 1) % this.cycleMessages.length;
168
+ this.message = this.cycleMessages[this.cycleIndex];
169
+ }
170
+ this.phase = "typing";
171
+ this.scheduleWordTick();
172
+ }
173
+ }
174
+ }
175
+ currentWord() {
176
+ return this.message;
177
+ }
178
+ // ── rendering ─────────────────────────────────────────────────────────────
179
+ renderAnimatedWord() {
180
+ const word = this.currentWord();
181
+ const visible = word.slice(0, this.visibleChars);
182
+ if (!visible)
183
+ return DIM + " " + RESET;
184
+ // Each char gets a colour position that travels through the gradient.
185
+ // The "wave" offset shifts every shimmerTick for a moving shimmer.
186
+ let out = "";
187
+ for (let i = 0; i < visible.length; i++) {
188
+ const wave = (i / Math.max(word.length - 1, 1));
189
+ // Slow rightward drift
190
+ const drift = (this.shimmerTick * 0.025) % 1;
191
+ const t = ((wave + drift) % 1);
192
+ const [r, g, b] = lerpColor(...this.gradStart, ...this.gradEnd, t);
193
+ // Last char being typed gets a bright flash
194
+ const isEdge = (this.phase === "typing" && i === visible.length - 1);
195
+ const char = isEdge
196
+ ? `${BOLD}${rgb(255, 255, 255, visible[i])}`
197
+ : rgb(r, g, b, visible[i]);
198
+ out += char;
199
+ }
200
+ return out;
44
201
  }
45
202
  updateDisplay() {
46
203
  const frame = this.frames[this.currentFrame];
47
- this.setText(`${this.spinnerColorFn(frame)} ${this.messageColorFn(this.message)}`);
48
- if (this.ui) {
204
+ const spinner = this.spinnerColorFn(frame);
205
+ const text = this.cycleMessages
206
+ ? this.renderAnimatedWord()
207
+ : this._messageColorFn(this.message);
208
+ this.setText(`${spinner} ${text}`);
209
+ if (this.ui)
49
210
  this.ui.requestRender();
50
- }
51
211
  }
52
212
  }
53
213
  //# sourceMappingURL=loader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/components/loader.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;GAEG;AACH,MAAM,OAAO,MAAO,SAAQ,IAAI;IAM/B,YACC,EAAO,EACC,cAAuC,EACvC,cAAuC,EACvC,UAAkB,YAAY;QAEtC,KAAK,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAJR,mBAAc,GAAd,cAAc,CAAyB;QACvC,mBAAc,GAAd,cAAc,CAAyB;QACvC,YAAO,GAAP,OAAO,CAAuB;QAT/B,WAAM,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAC5D,iBAAY,GAAG,CAAC,CAAC;QACjB,eAAU,GAA0B,IAAI,CAAC;QACzC,OAAE,GAAe,IAAI,CAAC;QAS7B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,KAAK,EAAE,CAAC;IACd,CAAC;IAED,MAAM,CAAC,KAAa;QACnB,OAAO,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,KAAK;QACJ,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;YAClC,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;YACjE,IAAI,CAAC,aAAa,EAAE,CAAC;QACtB,CAAC,EAAE,EAAE,CAAC,CAAC;IACR,CAAC;IAED,IAAI;QACH,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACxB,CAAC;IACF,CAAC;IAED,OAAO;QACN,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;IAChB,CAAC;IAED,UAAU,CAAC,OAAe;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,aAAa,EAAE,CAAC;IACtB,CAAC;IAEO,aAAa;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACnF,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;YACb,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;QACzB,CAAC;IACF,CAAC;CACD","sourcesContent":["import type { TUI } from \"../tui.js\";\nimport { Text } from \"./text.js\";\n\n/**\n * Loader component that updates every 80ms with spinning animation\n */\nexport class Loader extends Text {\n\tprivate frames = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n\tprivate currentFrame = 0;\n\tprivate intervalId: NodeJS.Timeout | null = null;\n\tprivate ui: TUI | null = null;\n\n\tconstructor(\n\t\tui: TUI,\n\t\tprivate spinnerColorFn: (str: string) => string,\n\t\tprivate messageColorFn: (str: string) => string,\n\t\tprivate message: string = \"Loading...\",\n\t) {\n\t\tsuper(\"\", 1, 0);\n\t\tthis.ui = ui;\n\t\tthis.start();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn [\"\", ...super.render(width)];\n\t}\n\n\tstart() {\n\t\tif (this.intervalId) {\n\t\t\tclearInterval(this.intervalId);\n\t\t}\n\t\tthis.updateDisplay();\n\t\tthis.intervalId = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.frames.length;\n\t\t\tthis.updateDisplay();\n\t\t}, 80);\n\t}\n\n\tstop() {\n\t\tif (this.intervalId) {\n\t\t\tclearInterval(this.intervalId);\n\t\t\tthis.intervalId = null;\n\t\t}\n\t}\n\n\tdispose() {\n\t\tthis.stop();\n\t\tthis.ui = null;\n\t}\n\n\tsetMessage(message: string) {\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay() {\n\t\tconst frame = this.frames[this.currentFrame];\n\t\tthis.setText(`${this.spinnerColorFn(frame)} ${this.messageColorFn(this.message)}`);\n\t\tif (this.ui) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/components/loader.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TUI } from \"../tui.js\";\nimport { Text } from \"./text.js\";\n\n// ─── ANSI helpers ─────────────────────────────────────────────────────────────\n\nconst ESC = \"\\x1b[\";\nconst RESET = \"\\x1b[0m\";\nconst DIM = \"\\x1b[2m\";\nconst BOLD = \"\\x1b[1m\";\n\n/** 24-bit foreground color */\nfunction rgb(r: number, g: number, b: number, s: string): string {\n\treturn `${ESC}38;2;${r};${g};${b}m${s}${RESET}`;\n}\n\n/** Interpolate between two RGB colors at position t ∈ [0,1] */\nfunction lerpColor(\n\tr1: number, g1: number, b1: number,\n\tr2: number, g2: number, b2: number,\n\tt: number,\n): [number, number, number] {\n\treturn [\n\t\tMath.round(r1 + (r2 - r1) * t),\n\t\tMath.round(g1 + (g2 - g1) * t),\n\t\tMath.round(b1 + (b2 - b1) * t),\n\t];\n}\n\n// ─── Timing constants ─────────────────────────────────────────────────────────\n\nconst TYPING_SPEED = 55; // ms per character typed\nconst HOLD_TIME = 5000; // ms to hold the full word\nconst ERASE_SPEED = 35; // ms per character erased\n\n// ─── Phase type ───────────────────────────────────────────────────────────────\n\ntype Phase = \"typing\" | \"hold\" | \"erase\";\n\n/**\n * Loader component — braille spinner + animated word cycling.\n *\n * Each word types in character by character (typewriter), holds with a moving\n * blue→cyan shimmer gradient, then erases right-to-left before the next word.\n */\nexport class Loader extends Text {\n\t// ── spinner ───────────────────────────────────────────────────────────────\n\tprivate frames = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n\tprivate currentFrame = 0;\n\tprivate spinnerIntervalId: NodeJS.Timeout | null = null;\n\tprivate ui: TUI | null = null;\n\n\t// ── message state ─────────────────────────────────────────────────────────\n\tprivate message: string = \"\";\n\n\t// ── word animation ────────────────────────────────────────────────────────\n\tprivate cycleMessages: string[] | null = null;\n\tprivate cycleIndex = 0;\n\tprivate phase: Phase = \"typing\";\n\tprivate visibleChars = 0;\n\tprivate wordAnimId: NodeJS.Timeout | null = null;\n\n\t// ── shimmer gradient: theme blueHigh → blueXhigh ─────────────────────────\n\tprivate readonly gradStart: [number, number, number] = [147, 197, 253]; // #93c5fd (blue-300)\n\tprivate readonly gradEnd: [number, number, number] = [191, 219, 254]; // #bfdbfe (blue-200)\n\n\t// Shimmer ticks independently at ~40ms for smooth gradient movement\n\tprivate shimmerTick = 0;\n\tprivate shimmerIntervalId: NodeJS.Timeout | null = null;\n\n\tconstructor(\n\t\tui: TUI,\n\t\tprivate spinnerColorFn: (str: string) => string,\n\t\tprivate _messageColorFn: (str: string) => string,\n\t\tmessage: string = \"Loading…\",\n\t) {\n\t\tsuper(\"\", 1, 0);\n\t\tthis.ui = ui;\n\t\tthis.message = message;\n\t\tthis.start();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn [\"\", ...super.render(width)];\n\t}\n\n\t// ── public API ────────────────────────────────────────────────────────────\n\n\tsetCycleMessages(messages: string[], _intervalMs = 3000) {\n\t\tthis.cycleMessages = [...messages];\n\t\tthis.cycleIndex = 0;\n\t\tthis.message = this.cycleMessages[0];\n\t\tif (this.spinnerIntervalId) {\n\t\t\tthis.startWordAnimation();\n\t\t}\n\t\tthis.updateDisplay();\n\t}\n\n\tclearCycleMessages() {\n\t\tthis.stopWordAnimation();\n\t\tthis.cycleMessages = null;\n\t}\n\n\tsetMessage(message: string) {\n\t\t// When an explicit message is set externally (e.g. \"Waiting for approval…\"),\n\t\t// pause the cycle so it doesn't immediately overwrite the override.\n\t\tthis.stopWordAnimation();\n\t\tthis.cycleMessages = null;\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tresumeCycle() {\n\t\tif (this.cycleMessages && !this.wordAnimId) {\n\t\t\tthis.startWordAnimation();\n\t\t}\n\t}\n\n\tstart() {\n\t\tif (this.spinnerIntervalId) clearInterval(this.spinnerIntervalId);\n\t\tthis.spinnerIntervalId = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.frames.length;\n\t\t\tthis.updateDisplay();\n\t\t}, 80);\n\n\t\t// Shimmer ticks independently at ~40ms for smooth gradient movement\n\t\tif (this.shimmerIntervalId) clearInterval(this.shimmerIntervalId);\n\t\tthis.shimmerIntervalId = setInterval(() => {\n\t\t\tthis.shimmerTick++;\n\t\t\tif (this.cycleMessages) this.updateDisplay();\n\t\t}, 40);\n\n\t\tif (this.cycleMessages) {\n\t\t\tthis.startWordAnimation();\n\t\t} else {\n\t\t\tthis.updateDisplay();\n\t\t}\n\t}\n\n\tstop() {\n\t\tif (this.spinnerIntervalId) { clearInterval(this.spinnerIntervalId); this.spinnerIntervalId = null; }\n\t\tif (this.shimmerIntervalId) { clearInterval(this.shimmerIntervalId); this.shimmerIntervalId = null; }\n\t\tthis.stopWordAnimation();\n\t}\n\n\tdispose() {\n\t\tthis.stop();\n\t\tthis.ui = null;\n\t}\n\n\t// ── word animation internals ──────────────────────────────────────────────\n\n\tprivate startWordAnimation() {\n\t\tthis.stopWordAnimation();\n\t\tthis.phase = \"typing\";\n\t\tthis.visibleChars = 0;\n\t\tthis.scheduleWordTick();\n\t}\n\n\tprivate stopWordAnimation() {\n\t\tif (this.wordAnimId) { clearTimeout(this.wordAnimId); this.wordAnimId = null; }\n\t}\n\n\tprivate scheduleWordTick() {\n\t\tconst word = this.currentWord();\n\n\t\tif (this.phase === \"typing\") {\n\t\t\tif (this.visibleChars < word.length) {\n\t\t\t\tthis.wordAnimId = setTimeout(() => {\n\t\t\t\t\tthis.visibleChars++;\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.scheduleWordTick();\n\t\t\t\t}, TYPING_SPEED);\n\t\t\t} else {\n\t\t\t\t// Full word shown → hold\n\t\t\t\tthis.phase = \"hold\";\n\t\t\t\tthis.wordAnimId = setTimeout(() => {\n\t\t\t\t\tthis.phase = \"erase\";\n\t\t\t\t\tthis.scheduleWordTick();\n\t\t\t\t}, HOLD_TIME);\n\t\t\t}\n\t\t} else if (this.phase === \"erase\") {\n\t\t\tif (this.visibleChars > 0) {\n\t\t\t\tthis.wordAnimId = setTimeout(() => {\n\t\t\t\t\tthis.visibleChars--;\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.scheduleWordTick();\n\t\t\t\t}, ERASE_SPEED);\n\t\t\t} else {\n\t\t\t\t// Advance to next word\n\t\t\t\tif (this.cycleMessages) {\n\t\t\t\t\tthis.cycleIndex = (this.cycleIndex + 1) % this.cycleMessages.length;\n\t\t\t\t\tthis.message = this.cycleMessages[this.cycleIndex];\n\t\t\t\t}\n\t\t\t\tthis.phase = \"typing\";\n\t\t\t\tthis.scheduleWordTick();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate currentWord(): string {\n\t\treturn this.message;\n\t}\n\n\t// ── rendering ─────────────────────────────────────────────────────────────\n\n\tprivate renderAnimatedWord(): string {\n\t\tconst word = this.currentWord();\n\t\tconst visible = word.slice(0, this.visibleChars);\n\t\tif (!visible) return DIM + \" \" + RESET;\n\n\t\t// Each char gets a colour position that travels through the gradient.\n\t\t// The \"wave\" offset shifts every shimmerTick for a moving shimmer.\n\t\tlet out = \"\";\n\t\tfor (let i = 0; i < visible.length; i++) {\n\t\t\tconst wave = (i / Math.max(word.length - 1, 1));\n\t\t\t// Slow rightward drift\n\t\t\tconst drift = (this.shimmerTick * 0.025) % 1;\n\t\t\tconst t = ((wave + drift) % 1);\n\t\t\tconst [r, g, b] = lerpColor(...this.gradStart, ...this.gradEnd, t);\n\n\t\t\t// Last char being typed gets a bright flash\n\t\t\tconst isEdge = (this.phase === \"typing\" && i === visible.length - 1);\n\t\t\tconst char = isEdge\n\t\t\t\t? `${BOLD}${rgb(255, 255, 255, visible[i])}`\n\t\t\t\t: rgb(r, g, b, visible[i]);\n\t\t\tout += char;\n\t\t}\n\t\treturn out;\n\t}\n\n\tprivate updateDisplay() {\n\t\tconst frame = this.frames[this.currentFrame];\n\t\tconst spinner = this.spinnerColorFn(frame);\n\t\tconst text = this.cycleMessages\n\t\t\t? this.renderAnimatedWord()\n\t\t\t: this._messageColorFn(this.message);\n\t\tthis.setText(`${spinner} ${text}`);\n\t\tif (this.ui) this.ui.requestRender();\n\t}\n}\n"]}
@@ -163,6 +163,10 @@ export class Editor implements Component, Focusable {
163
163
  private pastes: Map<number, string> = new Map();
164
164
  private pasteCounter: number = 0;
165
165
 
166
+ // Drag-and-drop image path markers (visual only; expanded back on submit)
167
+ private droppedImagePaths: Map<number, string> = new Map();
168
+ private droppedImageCounter: number = 0;
169
+
166
170
  // Bracketed paste mode buffering
167
171
  private pasteBuffer: string = "";
168
172
  private isInPaste: boolean = false;
@@ -877,9 +881,51 @@ export class Editor implements Component, Focusable {
877
881
  const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
878
882
  result = result.replace(markerRegex, pasteContent);
879
883
  }
884
+ return this.expandDroppedImageMarkers(result);
885
+ }
886
+
887
+ private expandDroppedImageMarkers(text: string): string {
888
+ let result = text;
889
+ for (const [imageId, imagePath] of this.droppedImagePaths) {
890
+ const markerRegex = new RegExp(`<Image${imageId}>`, "g");
891
+ result = result.replace(markerRegex, imagePath);
892
+ }
880
893
  return result;
881
894
  }
882
895
 
896
+ private extractDroppedImagePath(pastedText: string): string | null {
897
+ const trimmed = pastedText.trim();
898
+ if (!trimmed || /[\n\r\t]/.test(trimmed)) return null;
899
+
900
+ const unquoted =
901
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
902
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
903
+ ? trimmed.slice(1, -1)
904
+ : trimmed;
905
+
906
+ let maybePath = unquoted;
907
+ if (unquoted.startsWith("file://")) {
908
+ try {
909
+ maybePath = decodeURI(unquoted.replace(/^file:\/\//, ""));
910
+ } catch {
911
+ return null;
912
+ }
913
+ }
914
+ const normalizedPath = maybePath.replace(/\\ /g, " ");
915
+
916
+ if (!/\.(png|jpe?g|gif|webp|bmp|tiff?|svg|heic|heif|avif)$/i.test(normalizedPath)) {
917
+ return null;
918
+ }
919
+
920
+ if (!normalizedPath.startsWith("/") && !normalizedPath.startsWith("~/") && !normalizedPath.startsWith("./") && !normalizedPath.startsWith("../")) {
921
+ return null;
922
+ }
923
+
924
+ // Return the normalized filesystem path (not the raw trimmed input)
925
+ // This ensures file:// URIs are decoded and escaped spaces are normalized
926
+ return normalizedPath;
927
+ }
928
+
883
929
  getLines(): string[] {
884
930
  return [...this.state.lines];
885
931
  }
@@ -1059,16 +1105,29 @@ export class Editor implements Component, Focusable {
1059
1105
  .filter((char) => char === "\n" || char.charCodeAt(0) >= 32)
1060
1106
  .join("");
1061
1107
 
1062
- // If pasting a file path (starts with /, ~, or .) and the character before
1063
- // the cursor is a word character, prepend a space for better readability
1064
- if (/^[/~.]/.test(filteredText)) {
1108
+ // Check if we need a leading space for readability
1109
+ // This applies to file:// URIs, absolute paths (~, /), or relative paths (., ..)
1110
+ let needsLeadingSpace = false;
1111
+ if (/^(file:\/\/|[/~.])/.test(filteredText)) {
1065
1112
  const currentLine = this.state.lines[this.state.cursorLine] || "";
1066
1113
  const charBeforeCursor = this.state.cursorCol > 0 ? currentLine[this.state.cursorCol - 1] : "";
1067
1114
  if (charBeforeCursor && /\w/.test(charBeforeCursor)) {
1115
+ needsLeadingSpace = true;
1068
1116
  filteredText = ` ${filteredText}`;
1069
1117
  }
1070
1118
  }
1071
1119
 
1120
+ const droppedImagePath = this.extractDroppedImagePath(filteredText);
1121
+ if (droppedImagePath) {
1122
+ this.droppedImageCounter++;
1123
+ const imageId = this.droppedImageCounter;
1124
+ this.droppedImagePaths.set(imageId, droppedImagePath);
1125
+ // Preserve leading space for readability (e.g., "foo" -> "foo <Image1>" -> "foo /tmp/image.png")
1126
+ const toInsert = needsLeadingSpace ? ` <Image${imageId}>` : `<Image${imageId}>`;
1127
+ this.insertTextAtCursorInternal(toInsert);
1128
+ return;
1129
+ }
1130
+
1072
1131
  // Split into lines to check for large paste
1073
1132
  const pastedLines = filteredText.split("\n");
1074
1133
 
@@ -1138,10 +1197,13 @@ export class Editor implements Component, Focusable {
1138
1197
  const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
1139
1198
  result = result.replace(markerRegex, pasteContent);
1140
1199
  }
1200
+ result = this.expandDroppedImageMarkers(result);
1141
1201
 
1142
1202
  this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
1143
1203
  this.pastes.clear();
1144
1204
  this.pasteCounter = 0;
1205
+ this.droppedImagePaths.clear();
1206
+ this.droppedImageCounter = 0;
1145
1207
  this.historyIndex = -1;
1146
1208
  this.scrollOffset = 0;
1147
1209
  this.undoStack.clear();