@antelopejs/dms-ai 0.1.3 → 0.1.5

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 (245) hide show
  1. package/README.md +48 -29
  2. package/dist/channels/bridge.d.ts +2 -8
  3. package/dist/channels/bridge.js +26 -42
  4. package/dist/channels/bridge.js.map +1 -1
  5. package/dist/channels/channel-service.d.ts +2 -2
  6. package/dist/channels/channel-service.js +12 -31
  7. package/dist/channels/channel-service.js.map +1 -1
  8. package/dist/channels/registry.d.ts +2 -2
  9. package/dist/channels/registry.js +7 -7
  10. package/dist/channels/registry.js.map +1 -1
  11. package/dist/config.d.ts +1 -0
  12. package/dist/config.js +8 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/constants/channels.d.ts +5 -10
  15. package/dist/constants/channels.js +6 -10
  16. package/dist/constants/channels.js.map +1 -1
  17. package/dist/constants/sidecar.d.ts +2 -1
  18. package/dist/constants/sidecar.js +3 -2
  19. package/dist/constants/sidecar.js.map +1 -1
  20. package/dist/index.js +8 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/lifecycle/build-id.d.ts +1 -0
  23. package/dist/lifecycle/build-id.js +32 -0
  24. package/dist/lifecycle/build-id.js.map +1 -0
  25. package/dist/lifecycle/sidecar-socket.d.ts +1 -1
  26. package/dist/lifecycle/sidecar-socket.js +2 -2
  27. package/dist/lifecycle/sidecar-socket.js.map +1 -1
  28. package/dist/lifecycle/spawn-sidecar.d.ts +0 -1
  29. package/dist/lifecycle/spawn-sidecar.js +5 -27
  30. package/dist/lifecycle/spawn-sidecar.js.map +1 -1
  31. package/dist/pages/settings/page.js.map +1 -1
  32. package/dist/routes/channels.d.ts +3 -3
  33. package/dist/routes/channels.js +11 -12
  34. package/dist/routes/channels.js.map +1 -1
  35. package/dist/routes/index.d.ts +0 -1
  36. package/dist/routes/index.js +0 -1
  37. package/dist/routes/index.js.map +1 -1
  38. package/frontend-vue/.prettierignore +3 -0
  39. package/frontend-vue/.prettierrc +10 -0
  40. package/frontend-vue/app/chat/components/ChatView.vue +582 -0
  41. package/frontend-vue/app/chat/components/ComposerInput.vue +742 -0
  42. package/frontend-vue/app/chat/components/ConnectionBanner.vue +105 -0
  43. package/frontend-vue/app/chat/components/ConversationDrawer.vue +266 -0
  44. package/frontend-vue/app/chat/components/MarkdownContent.vue +196 -0
  45. package/frontend-vue/app/chat/components/MessageList.vue +474 -0
  46. package/frontend-vue/app/chat/components/PermissionRequests.vue +323 -0
  47. package/frontend-vue/app/chat/components/QuestionPrompt.vue +275 -0
  48. package/frontend-vue/app/chat/components/QueuedMessages.vue +109 -0
  49. package/frontend-vue/app/chat/components/TodoList.vue +291 -0
  50. package/frontend-vue/app/chat/components/ToolCallEntry.vue +218 -0
  51. package/frontend-vue/app/chat/components/ToolCluster.vue +282 -0
  52. package/frontend-vue/app/chat/composables/useChatChannel.ts +116 -0
  53. package/frontend-vue/app/chat/composables/useConversation.ts +838 -0
  54. package/frontend-vue/app/chat/composables/useConversationId.ts +34 -0
  55. package/frontend-vue/app/chat/composables/useConversationList.ts +56 -0
  56. package/frontend-vue/app/chat/composables/usePermissionQueue.ts +59 -0
  57. package/frontend-vue/app/chat/composables/useQuestionQueue.ts +50 -0
  58. package/frontend-vue/app/chat/composables/useRunClock.ts +28 -0
  59. package/frontend-vue/app/chat/composables/useSettings.ts +67 -0
  60. package/frontend-vue/app/chat/constants/attachments.ts +16 -0
  61. package/frontend-vue/app/chat/constants/connection-banner.ts +7 -0
  62. package/frontend-vue/app/chat/constants/conversation-drawer.ts +13 -0
  63. package/frontend-vue/app/chat/constants/conversation.ts +75 -0
  64. package/frontend-vue/app/chat/constants/permissions.ts +50 -0
  65. package/frontend-vue/app/chat/constants/protocol.ts +48 -0
  66. package/frontend-vue/app/chat/constants/run-status.ts +50 -0
  67. package/frontend-vue/app/chat/constants/settings.ts +60 -0
  68. package/frontend-vue/app/chat/types/conversation.ts +87 -0
  69. package/frontend-vue/app/chat/types/permission.ts +7 -0
  70. package/frontend-vue/app/chat/types/question.ts +17 -0
  71. package/frontend-vue/app/chat/types/settings.ts +27 -0
  72. package/frontend-vue/app/chat/utils/attachments.ts +62 -0
  73. package/frontend-vue/app/chat/utils/format.ts +14 -0
  74. package/frontend-vue/app/chat/utils/ids.ts +34 -0
  75. package/frontend-vue/app/chat/utils/markdown.ts +188 -0
  76. package/frontend-vue/app/chat/utils/run-status.ts +90 -0
  77. package/frontend-vue/app/chat/utils/todos.ts +64 -0
  78. package/frontend-vue/app/chat/utils/tool-summary.ts +53 -0
  79. package/frontend-vue/app/components/ActivityView.vue +34 -34
  80. package/frontend-vue/app/components/ChatPanel.vue +276 -0
  81. package/frontend-vue/app/components/SettingsView.vue +127 -130
  82. package/frontend-vue/app/components/SkillsView.vue +55 -53
  83. package/frontend-vue/app/plugins/ai.client.ts +145 -129
  84. package/frontend-vue/app/runtime/assistant-session.ts +21 -0
  85. package/frontend-vue/app/runtime/backoff.ts +11 -14
  86. package/frontend-vue/app/runtime/channel-client.ts +130 -141
  87. package/frontend-vue/app/runtime/chat-transport.ts +25 -55
  88. package/frontend-vue/app/runtime/constants.ts +70 -60
  89. package/frontend-vue/app/runtime/current-page.ts +49 -15
  90. package/frontend-vue/app/runtime/event-stream.ts +35 -35
  91. package/frontend-vue/app/runtime/frame-router.ts +30 -0
  92. package/frontend-vue/app/runtime/header-action.ts +18 -18
  93. package/frontend-vue/app/runtime/host-commands.ts +57 -56
  94. package/frontend-vue/app/runtime/listeners.ts +31 -0
  95. package/frontend-vue/app/runtime/navigation-complete.ts +7 -7
  96. package/frontend-vue/app/runtime/overlay-prefs.ts +36 -36
  97. package/frontend-vue/app/runtime/panel-state.ts +88 -0
  98. package/frontend-vue/app/runtime/session-gate.ts +8 -8
  99. package/frontend-vue/app/runtime/shortcuts.ts +12 -12
  100. package/frontend-vue/app/runtime/sidecar-status.ts +61 -61
  101. package/frontend-vue/app/runtime/typed-message.ts +11 -0
  102. package/frontend-vue/dms.frontend.ts +12 -12
  103. package/frontend-vue/eslint.config.mjs +13 -13
  104. package/frontend-vue/package.json +11 -4
  105. package/frontend-vue/pnpm-lock.yaml +298 -96
  106. package/frontend-vue/tests/backoff.test.ts +26 -30
  107. package/frontend-vue/tests/channel-client.test.ts +187 -188
  108. package/frontend-vue/tests/chat/markdown.test.ts +271 -0
  109. package/frontend-vue/tests/chat/useChatChannel.test.ts +124 -0
  110. package/frontend-vue/tests/chat/useConversation.test.ts +283 -0
  111. package/frontend-vue/tests/chat-panel.test.ts +354 -0
  112. package/frontend-vue/tests/chat-transport.test.ts +84 -57
  113. package/frontend-vue/tests/host-commands.test.ts +126 -87
  114. package/frontend-vue/tests/navigation.test.ts +65 -34
  115. package/frontend-vue/tests/overlay-prefs.test.ts +49 -49
  116. package/frontend-vue/tests/panel-state.test.ts +58 -0
  117. package/frontend-vue/tests/session-gate.test.ts +33 -33
  118. package/frontend-vue/tests/sidecar-guest.test.ts +46 -46
  119. package/frontend-vue/tests/sidecar-status.test.ts +29 -29
  120. package/frontend-vue/tsconfig.chat.json +16 -0
  121. package/frontend-vue/vitest.config.ts +6 -4
  122. package/package.json +5 -5
  123. package/sidecar/dist/agent/effective-mode.d.ts +2 -2
  124. package/sidecar/dist/agent/effective-mode.js +1 -1
  125. package/sidecar/dist/agent/effective-mode.js.map +1 -1
  126. package/sidecar/dist/agent/host-context.js +7 -9
  127. package/sidecar/dist/agent/host-context.js.map +1 -1
  128. package/sidecar/dist/agent/permission-bus.d.ts +1 -1
  129. package/sidecar/dist/agent/permission-bus.js +2 -2
  130. package/sidecar/dist/agent/permission-bus.js.map +1 -1
  131. package/sidecar/dist/agent/provider.d.ts +2 -2
  132. package/sidecar/dist/agent/question-bus.d.ts +1 -1
  133. package/sidecar/dist/agent/question-bus.js +2 -2
  134. package/sidecar/dist/agent/question-bus.js.map +1 -1
  135. package/sidecar/dist/agent/runner.d.ts +2 -2
  136. package/sidecar/dist/agent/runner.js +24 -8
  137. package/sidecar/dist/agent/runner.js.map +1 -1
  138. package/sidecar/dist/agent/session.d.ts +2 -2
  139. package/sidecar/dist/agent/session.js +49 -9
  140. package/sidecar/dist/agent/session.js.map +1 -1
  141. package/sidecar/dist/agent/switching-runner.js +10 -5
  142. package/sidecar/dist/agent/switching-runner.js.map +1 -1
  143. package/sidecar/dist/agent/turn-end-reasons.d.ts +2 -0
  144. package/sidecar/dist/agent/turn-end-reasons.js +6 -1
  145. package/sidecar/dist/agent/turn-end-reasons.js.map +1 -1
  146. package/sidecar/dist/constants/agent.d.ts +4 -1
  147. package/sidecar/dist/constants/agent.js +4 -1
  148. package/sidecar/dist/constants/agent.js.map +1 -1
  149. package/sidecar/dist/constants/cli.d.ts +0 -1
  150. package/sidecar/dist/constants/cli.js +0 -1
  151. package/sidecar/dist/constants/cli.js.map +1 -1
  152. package/sidecar/dist/constants/codex.d.ts +5 -2
  153. package/sidecar/dist/constants/codex.js +5 -2
  154. package/sidecar/dist/constants/codex.js.map +1 -1
  155. package/sidecar/dist/constants/http.d.ts +0 -9
  156. package/sidecar/dist/constants/http.js +0 -19
  157. package/sidecar/dist/constants/http.js.map +1 -1
  158. package/sidecar/dist/constants/mcp.d.ts +1 -1
  159. package/sidecar/dist/constants/mcp.js +1 -1
  160. package/sidecar/dist/constants/mcp.js.map +1 -1
  161. package/sidecar/dist/constants/paths.d.ts +0 -1
  162. package/sidecar/dist/constants/paths.js +0 -1
  163. package/sidecar/dist/constants/paths.js.map +1 -1
  164. package/sidecar/dist/constants/ws.d.ts +3 -4
  165. package/sidecar/dist/constants/ws.js +3 -4
  166. package/sidecar/dist/constants/ws.js.map +1 -1
  167. package/sidecar/dist/index.d.ts +0 -1
  168. package/sidecar/dist/index.js +6 -10
  169. package/sidecar/dist/index.js.map +1 -1
  170. package/sidecar/dist/mcp/tools/find-pages-using.js +5 -10
  171. package/sidecar/dist/mcp/tools/find-pages-using.js.map +1 -1
  172. package/sidecar/dist/mcp/types.d.ts +1 -1
  173. package/sidecar/dist/pages/page-filepath.d.ts +4 -0
  174. package/sidecar/dist/pages/page-filepath.js +28 -0
  175. package/sidecar/dist/pages/page-filepath.js.map +1 -0
  176. package/sidecar/dist/prompts/agent.d.ts +1 -1
  177. package/sidecar/dist/prompts/agent.js +1 -1
  178. package/sidecar/dist/prompts/agent.js.map +1 -1
  179. package/sidecar/dist/protocol/events.js +2 -2
  180. package/sidecar/dist/protocol/events.js.map +1 -1
  181. package/sidecar/dist/protocol/messages.d.ts +4 -15
  182. package/sidecar/dist/protocol/messages.js +4 -11
  183. package/sidecar/dist/protocol/messages.js.map +1 -1
  184. package/sidecar/dist/providers/claude/config.d.ts +2 -2
  185. package/sidecar/dist/providers/claude/config.js +2 -2
  186. package/sidecar/dist/providers/claude/config.js.map +1 -1
  187. package/sidecar/dist/providers/claude/provider.js +1 -1
  188. package/sidecar/dist/providers/claude/provider.js.map +1 -1
  189. package/sidecar/dist/providers/codex/config.d.ts +3 -2
  190. package/sidecar/dist/providers/codex/config.js +3 -3
  191. package/sidecar/dist/providers/codex/config.js.map +1 -1
  192. package/sidecar/dist/providers/codex/process.js +107 -74
  193. package/sidecar/dist/providers/codex/process.js.map +1 -1
  194. package/sidecar/dist/providers/codex/provider.js +68 -31
  195. package/sidecar/dist/providers/codex/provider.js.map +1 -1
  196. package/sidecar/dist/providers/codex/skills.d.ts +1 -0
  197. package/sidecar/dist/providers/codex/skills.js +4 -1
  198. package/sidecar/dist/providers/codex/skills.js.map +1 -1
  199. package/sidecar/dist/providers/codex/stop.d.ts +4 -0
  200. package/sidecar/dist/providers/codex/stop.js +71 -0
  201. package/sidecar/dist/providers/codex/stop.js.map +1 -0
  202. package/sidecar/dist/server/{iframe-socket-registry.d.ts → chat-socket-registry.d.ts} +3 -2
  203. package/sidecar/dist/server/{iframe-socket-registry.js → chat-socket-registry.js} +4 -3
  204. package/sidecar/dist/server/chat-socket-registry.js.map +1 -0
  205. package/sidecar/dist/server/client-auth.js +2 -14
  206. package/sidecar/dist/server/client-auth.js.map +1 -1
  207. package/sidecar/dist/server/host-command-router.d.ts +5 -0
  208. package/sidecar/dist/server/host-command-router.js +4 -0
  209. package/sidecar/dist/server/host-command-router.js.map +1 -0
  210. package/sidecar/dist/server/host-socket-registry.d.ts +1 -1
  211. package/sidecar/dist/server/host-socket-registry.js +16 -13
  212. package/sidecar/dist/server/host-socket-registry.js.map +1 -1
  213. package/sidecar/dist/server/http.d.ts +0 -1
  214. package/sidecar/dist/server/http.js +1 -44
  215. package/sidecar/dist/server/http.js.map +1 -1
  216. package/sidecar/dist/server/routing.d.ts +5 -6
  217. package/sidecar/dist/server/routing.js +18 -28
  218. package/sidecar/dist/server/routing.js.map +1 -1
  219. package/sidecar/dist/server/ws.d.ts +2 -2
  220. package/sidecar/dist/server/ws.js +41 -39
  221. package/sidecar/dist/server/ws.js.map +1 -1
  222. package/sidecar/dist/state/host-state.d.ts +10 -2
  223. package/sidecar/dist/state/host-state.js +13 -3
  224. package/sidecar/dist/state/host-state.js.map +1 -1
  225. package/sidecar/dist/state/settings-store.js +2 -2
  226. package/sidecar/dist/state/settings-store.js.map +1 -1
  227. package/sidecar/dist/state/settings-types.d.ts +3 -3
  228. package/sidecar/dist/state/settings-types.js +1 -1
  229. package/sidecar/dist/state/settings-types.js.map +1 -1
  230. package/sidecar/package.json +3 -5
  231. package/dist/chatbox/passthrough.d.ts +0 -3
  232. package/dist/chatbox/passthrough.js +0 -53
  233. package/dist/chatbox/passthrough.js.map +0 -1
  234. package/dist/constants/chatbox.d.ts +0 -10
  235. package/dist/constants/chatbox.js +0 -35
  236. package/dist/constants/chatbox.js.map +0 -1
  237. package/dist/routes/chatbox.d.ts +0 -7
  238. package/dist/routes/chatbox.js +0 -47
  239. package/dist/routes/chatbox.js.map +0 -1
  240. package/frontend-vue/app/runtime/overlay.ts +0 -400
  241. package/frontend-vue/app/runtime/theme-bridge.ts +0 -189
  242. package/sidecar/chatbox/dist/assets/index-C8u7mkHn.css +0 -1
  243. package/sidecar/chatbox/dist/assets/index-tdZvY7ee.js +0 -52
  244. package/sidecar/chatbox/dist/index.html +0 -13
  245. package/sidecar/dist/server/iframe-socket-registry.js.map +0 -1
@@ -0,0 +1,87 @@
1
+ import type { ACTIVITY_KINDS } from "../constants/run-status";
2
+ import type { PendingAttachment } from "../utils/attachments";
3
+
4
+ export type MessageRole = "user" | "assistant" | "tool" | "error";
5
+ export type ToolStatus = "pending" | "success" | "error";
6
+ export type ActivityKind = (typeof ACTIVITY_KINDS)[keyof typeof ACTIVITY_KINDS];
7
+
8
+ /**
9
+ * A file attached to a user message. `dataUrl` is only present for messages
10
+ * sent in this session (drives the image thumbnail); after a reload the
11
+ * snapshot carries just the metadata, so it is absent.
12
+ */
13
+ export interface MessageAttachment {
14
+ name: string;
15
+ mimeType: string;
16
+ size: number;
17
+ dataUrl?: string;
18
+ }
19
+
20
+ export interface UserMessage {
21
+ id: string;
22
+ role: "user";
23
+ content: string;
24
+ attachments?: MessageAttachment[];
25
+ isUnsent?: boolean;
26
+ timestampMs: number;
27
+ }
28
+
29
+ export interface AssistantMessage {
30
+ id: string;
31
+ role: "assistant";
32
+ content: string;
33
+ timestampMs: number;
34
+ }
35
+
36
+ /** Why a run stopped or a message could not be sent, with a way to retry. */
37
+ export interface ErrorMessage {
38
+ id: string;
39
+ role: "error";
40
+ content: string;
41
+ isRetryable: boolean;
42
+ timestampMs: number;
43
+ }
44
+
45
+ /**
46
+ * What the running turn is doing, as last reported by the sidecar.
47
+ * `receivedAtMs` is the local time of that report, from which the elapsed and
48
+ * quiet durations keep counting until the next one.
49
+ */
50
+ export interface RunProgress {
51
+ activity: ActivityKind;
52
+ detail?: string;
53
+ elapsedMs: number;
54
+ idleMs: number;
55
+ receivedAtMs: number;
56
+ }
57
+
58
+ export interface ToolCallMessage {
59
+ id: string;
60
+ role: "tool";
61
+ callId: string;
62
+ toolName: string;
63
+ args: unknown;
64
+ result?: unknown;
65
+ status: ToolStatus;
66
+ timestampMs: number;
67
+ }
68
+
69
+ export type ConversationMessage =
70
+ | UserMessage
71
+ | AssistantMessage
72
+ | ToolCallMessage
73
+ | ErrorMessage;
74
+
75
+ export interface QueuedMessage {
76
+ id: string;
77
+ content: string;
78
+ attachments: PendingAttachment[];
79
+ }
80
+
81
+ export interface ConversationSummary {
82
+ id: string;
83
+ title: string;
84
+ createdAtMs: number;
85
+ updatedAtMs: number;
86
+ messageCount: number;
87
+ }
@@ -0,0 +1,7 @@
1
+ export interface PermissionRequestData {
2
+ requestId: string;
3
+ conversationId: string;
4
+ toolName: string;
5
+ args: unknown;
6
+ summary: string;
7
+ }
@@ -0,0 +1,17 @@
1
+ export interface QuestionOptionData {
2
+ label: string;
3
+ description: string;
4
+ preview?: string;
5
+ }
6
+
7
+ export interface QuestionData {
8
+ question: string;
9
+ header: string;
10
+ options: QuestionOptionData[];
11
+ }
12
+
13
+ export interface QuestionRequestData {
14
+ requestId: string;
15
+ conversationId: string;
16
+ questions: QuestionData[];
17
+ }
@@ -0,0 +1,27 @@
1
+ export type ChatMode = "normal" | "acceptEdits" | "plan" | "auto";
2
+ export type ThinkingLevel = "off" | "low" | "medium" | "high";
3
+ export type GenerationMode = "safe" | "vibe";
4
+ export type ProviderName = "claude" | "codex";
5
+
6
+ export interface ProviderAvailability {
7
+ available: boolean;
8
+ /** Why the provider cannot be picked; absent when it is available. */
9
+ reason?: string;
10
+ }
11
+
12
+ export interface AppSettings {
13
+ provider: ProviderName;
14
+ mode: ChatMode;
15
+ thinking: ThinkingLevel;
16
+ generationMode: GenerationMode;
17
+ allowLocalSkills: boolean;
18
+ /**
19
+ * Read-only capability pushed by the sidecar: whether the Builder is present.
20
+ */
21
+ builderAvailable: boolean;
22
+ /**
23
+ * Read-only capability pushed by the sidecar: which backends this install can
24
+ * actually drive.
25
+ */
26
+ providers: Record<ProviderName, ProviderAvailability>;
27
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ INLINE_IMAGE_MIME_TYPES,
3
+ MAX_ATTACHMENT_BYTES,
4
+ } from "../constants/attachments";
5
+ import { newUuid } from "./ids";
6
+
7
+ /**
8
+ * A file the user has staged in the composer. `data` is the base64 payload (no
9
+ * `data:` prefix) sent to the sidecar; `dataUrl` is the full data URL kept for
10
+ * local image previews.
11
+ */
12
+ export interface PendingAttachment {
13
+ id: string;
14
+ name: string;
15
+ mimeType: string;
16
+ size: number;
17
+ data: string;
18
+ dataUrl: string;
19
+ }
20
+
21
+ export function isInlineImage(mimeType: string): boolean {
22
+ return (INLINE_IMAGE_MIME_TYPES as readonly string[]).includes(mimeType);
23
+ }
24
+
25
+ export function formatBytes(size: number): string {
26
+ if (size < 1024) return `${size} B`;
27
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(0)} KB`;
28
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
29
+ }
30
+
31
+ export function exceedsSizeLimit(file: File): boolean {
32
+ return file.size > MAX_ATTACHMENT_BYTES;
33
+ }
34
+
35
+ /** The base64 payload of a data URL, without its `data:…;base64,` prefix. */
36
+ export function splitDataUrl(dataUrl: string): string {
37
+ const comma = dataUrl.indexOf(",");
38
+ return comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
39
+ }
40
+
41
+ /**
42
+ * Read a File into a PendingAttachment (base64 + data URL) via FileReader. The
43
+ * browser never exposes the real disk path, so we always carry the bytes.
44
+ */
45
+ export function readFileAsAttachment(file: File): Promise<PendingAttachment> {
46
+ return new Promise((resolve, reject) => {
47
+ const reader = new FileReader();
48
+ reader.onerror = () => reject(reader.error ?? new Error("read failed"));
49
+ reader.onload = () => {
50
+ const dataUrl = String(reader.result);
51
+ resolve({
52
+ id: newUuid(),
53
+ name: file.name,
54
+ mimeType: file.type || "application/octet-stream",
55
+ size: file.size,
56
+ data: splitDataUrl(dataUrl),
57
+ dataUrl,
58
+ });
59
+ };
60
+ reader.readAsDataURL(file);
61
+ });
62
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Render an arbitrary tool argument / result value for display: strings pass
3
+ * through, everything else is pretty-printed JSON, with a safe fallback for
4
+ * values JSON can't serialize (e.g. circular refs).
5
+ */
6
+ export function stringifyValue(value: unknown): string {
7
+ if (value === undefined) return "";
8
+ if (typeof value === "string") return value;
9
+ try {
10
+ return JSON.stringify(value, null, 2);
11
+ } catch {
12
+ return String(value);
13
+ }
14
+ }
@@ -0,0 +1,34 @@
1
+ const UUID_BYTE_COUNT = 16;
2
+ const VERSION_BYTE_INDEX = 6;
3
+ const VARIANT_BYTE_INDEX = 8;
4
+ const VERSION_MASK = 0x0f;
5
+ const VERSION_4 = 0x40;
6
+ const VARIANT_MASK = 0x3f;
7
+ const VARIANT_RFC_4122 = 0x80;
8
+ const HEX_RADIX = 16;
9
+ const HEX_BYTE_WIDTH = 2;
10
+ const UUID_GROUP_ENDS = [8, 12, 16, 20, 32];
11
+
12
+ function formatUuid(bytes: Uint8Array): string {
13
+ const hex = Array.from(bytes, (byte) =>
14
+ byte.toString(HEX_RADIX).padStart(HEX_BYTE_WIDTH, "0"),
15
+ ).join("");
16
+ return UUID_GROUP_ENDS.map((end, index) =>
17
+ hex.slice(UUID_GROUP_ENDS[index - 1] ?? 0, end),
18
+ ).join("-");
19
+ }
20
+
21
+ /**
22
+ * A random v4 UUID. `crypto.randomUUID` only exists in secure contexts, and the
23
+ * chat is also opened over plain HTTP from another machine (a LAN or Tailscale
24
+ * address), where `crypto.getRandomValues` is still there.
25
+ */
26
+ export function newUuid(): string {
27
+ if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
28
+ const bytes = crypto.getRandomValues(new Uint8Array(UUID_BYTE_COUNT));
29
+ bytes[VERSION_BYTE_INDEX] =
30
+ (bytes[VERSION_BYTE_INDEX] & VERSION_MASK) | VERSION_4;
31
+ bytes[VARIANT_BYTE_INDEX] =
32
+ (bytes[VARIANT_BYTE_INDEX] & VARIANT_MASK) | VARIANT_RFC_4122;
33
+ return formatUuid(bytes);
34
+ }
@@ -0,0 +1,188 @@
1
+ import DOMPurify, { type Config, type DOMPurify as Purifier } from "dompurify";
2
+ import MarkdownIt, { type Token } from "markdown-it";
3
+
4
+ const ALLOWED_TAGS = [
5
+ "a",
6
+ "blockquote",
7
+ "br",
8
+ "code",
9
+ "del",
10
+ "em",
11
+ "h1",
12
+ "h2",
13
+ "h3",
14
+ "h4",
15
+ "h5",
16
+ "h6",
17
+ "hr",
18
+ "li",
19
+ "ol",
20
+ "p",
21
+ "pre",
22
+ "s",
23
+ "strong",
24
+ "table",
25
+ "tbody",
26
+ "td",
27
+ "th",
28
+ "thead",
29
+ "tr",
30
+ "ul",
31
+ ];
32
+
33
+ const ALLOWED_ATTR = ["href", "title", "target", "rel", "start"];
34
+
35
+ const SAFE_URL = /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i;
36
+
37
+ const SANITIZE_CONFIG: Config = {
38
+ ALLOWED_TAGS,
39
+ ALLOWED_ATTR,
40
+ ALLOWED_URI_REGEXP: SAFE_URL,
41
+ ALLOW_DATA_ATTR: false,
42
+ ALLOW_ARIA_ATTR: false,
43
+ ALLOW_UNKNOWN_PROTOCOLS: false,
44
+ };
45
+
46
+ const LINK_TARGET = "_blank";
47
+ const LINK_REL = "noopener noreferrer";
48
+ const NEWLINES = /\r\n?/g;
49
+ const LINE_BREAK = "\n";
50
+
51
+ const md = new MarkdownIt({
52
+ html: false,
53
+ linkify: true,
54
+ breaks: true,
55
+ });
56
+
57
+ md.validateLink = (url) => SAFE_URL.test(url.trim());
58
+
59
+ md.renderer.rules.link_open = (tokens, idx, options, _env, self) => {
60
+ tokens[idx].attrSet("target", LINK_TARGET);
61
+ tokens[idx].attrSet("rel", LINK_REL);
62
+ return self.renderToken(tokens, idx, options);
63
+ };
64
+
65
+ function imageDestination(src: string): string {
66
+ try {
67
+ return new URL(src).host || src;
68
+ } catch {
69
+ return src;
70
+ }
71
+ }
72
+
73
+ function imageLinkLabel(alt: string, src: string): string {
74
+ const destination = imageDestination(src);
75
+ return alt ? `${alt} (${destination})` : destination;
76
+ }
77
+
78
+ md.renderer.rules.image = (tokens, idx, options, env, self) => {
79
+ const token = tokens[idx];
80
+ const src = token.attrGet("src") ?? "";
81
+ const alt = self.renderInlineAsText(token.children ?? [], options, env);
82
+ const label = md.utils.escapeHtml(imageLinkLabel(alt, src));
83
+ return `<a href="${md.utils.escapeHtml(src)}" target="${LINK_TARGET}" rel="${LINK_REL}">${label}</a>`;
84
+ };
85
+
86
+ let purifier: Purifier | null = null;
87
+
88
+ function getPurifier(): Purifier {
89
+ if (purifier !== null) return purifier;
90
+ purifier = DOMPurify(globalThis.window);
91
+ purifier.addHook("afterSanitizeAttributes", (node) => {
92
+ if (node.tagName !== "A" || !node.hasAttribute("href")) return;
93
+ node.setAttribute("target", LINK_TARGET);
94
+ node.setAttribute("rel", LINK_REL);
95
+ });
96
+ return purifier;
97
+ }
98
+
99
+ function sanitize(html: string): string {
100
+ return getPurifier().sanitize(html, SANITIZE_CONFIG);
101
+ }
102
+
103
+ function normalize(source: string): string {
104
+ return (source ?? "").replace(NEWLINES, LINE_BREAK);
105
+ }
106
+
107
+ /** Model output as sanitized HTML, whole. */
108
+ export function renderMarkdown(source: string): string {
109
+ return sanitize(md.render(normalize(source)));
110
+ }
111
+
112
+ interface TopLevelBlock {
113
+ tokens: Token[];
114
+ startLine: number;
115
+ }
116
+
117
+ function splitTopLevelBlocks(tokens: Token[]): TopLevelBlock[] {
118
+ const blocks: TopLevelBlock[] = [];
119
+ for (const token of tokens) {
120
+ const map = token.map;
121
+ const opensBlock =
122
+ token.level === 0 && token.nesting !== -1 && map !== null;
123
+ if (opensBlock) blocks.push({ tokens: [token], startLine: map[0] });
124
+ else blocks.at(-1)?.tokens.push(token);
125
+ }
126
+ return blocks;
127
+ }
128
+
129
+ function lineOffset(text: string, line: number): number {
130
+ let offset = 0;
131
+ for (let index = 0; index < line; index += 1) {
132
+ const next = text.indexOf(LINE_BREAK, offset);
133
+ if (next === -1) return text.length;
134
+ offset = next + 1;
135
+ }
136
+ return offset;
137
+ }
138
+
139
+ export interface MarkdownBlocks {
140
+ /** Top-level blocks that the text still to come can no longer change. */
141
+ settled: readonly string[];
142
+ /** The last block, which the next text may still extend. */
143
+ open: string;
144
+ }
145
+
146
+ export interface IncrementalMarkdown {
147
+ /** Renders `source`, reusing the settled blocks of the text it extends. */
148
+ render: (source: string) => MarkdownBlocks;
149
+ }
150
+
151
+ interface IncrementalState {
152
+ settledSource: string;
153
+ settled: string[];
154
+ env: Record<string, unknown>;
155
+ }
156
+
157
+ function renderBlock(block: TopLevelBlock, state: IncrementalState): string {
158
+ return sanitize(md.renderer.render(block.tokens, md.options, state.env));
159
+ }
160
+
161
+ function renderIncrement(
162
+ state: IncrementalState,
163
+ text: string,
164
+ ): MarkdownBlocks {
165
+ if (!text.startsWith(state.settledSource)) {
166
+ state.settledSource = "";
167
+ state.settled = [];
168
+ state.env = {};
169
+ }
170
+ const rest = text.slice(state.settledSource.length);
171
+ const blocks = splitTopLevelBlocks(md.parse(rest, state.env));
172
+ const open = blocks.pop();
173
+ if (open === undefined) return { settled: [...state.settled], open: "" };
174
+ for (const block of blocks) state.settled.push(renderBlock(block, state));
175
+ state.settledSource += rest.slice(0, lineOffset(rest, open.startLine));
176
+ return { settled: [...state.settled], open: renderBlock(open, state) };
177
+ }
178
+
179
+ /**
180
+ * Renders a message while it streams at a cost bounded by its last block: a
181
+ * top-level block is settled once markdown-it starts the next one, and is then
182
+ * rendered and sanitized once. Re-rendering the whole message on every chunk
183
+ * made a long answer quadratic, on the dashboard's own thread.
184
+ */
185
+ export function createIncrementalMarkdown(): IncrementalMarkdown {
186
+ const state: IncrementalState = { settledSource: "", settled: [], env: {} };
187
+ return { render: (source) => renderIncrement(state, normalize(source)) };
188
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ ACTIVITY_ELLIPSIS,
3
+ ACTIVITY_KINDS,
4
+ ACTIVITY_LABELS,
5
+ DEFAULT_ACTIVITY_LABEL,
6
+ QUIET_NOTICE_AFTER_MS,
7
+ SILENT_ACTIVITIES,
8
+ STALL_AFTER_MS,
9
+ TOOL_DETAIL_ACTIVITIES,
10
+ } from "../constants/run-status";
11
+ import type { ActivityKind, RunProgress } from "../types/conversation";
12
+ import { toolLabel } from "./tool-summary";
13
+
14
+ const MS_PER_SECOND = 1_000;
15
+ const SECONDS_PER_MINUTE = 60;
16
+ const SECONDS_WIDTH = 2;
17
+ const DETAIL_SEPARATOR = " · ";
18
+
19
+ const KNOWN_ACTIVITIES: readonly string[] = Object.values(ACTIVITY_KINDS);
20
+
21
+ /** A reported activity, read as thinking when this chat does not know it. */
22
+ export function toActivityKind(reported: string): ActivityKind {
23
+ if (!KNOWN_ACTIVITIES.includes(reported)) return ACTIVITY_KINDS.THINKING;
24
+ return reported as ActivityKind;
25
+ }
26
+
27
+ /** A duration as m:ss. */
28
+ export function formatClock(ms: number): string {
29
+ const totalSeconds = Math.max(0, Math.floor(ms / MS_PER_SECOND));
30
+ const minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);
31
+ const seconds = String(totalSeconds % SECONDS_PER_MINUTE).padStart(
32
+ SECONDS_WIDTH,
33
+ "0",
34
+ );
35
+ return `${minutes}:${seconds}`;
36
+ }
37
+
38
+ function describeDetail(progress: RunProgress): string {
39
+ if (progress.detail === undefined || progress.detail === "") return "";
40
+ const namesTool = TOOL_DETAIL_ACTIVITIES.includes(progress.activity);
41
+ const detail = namesTool ? toolLabel(progress.detail) : progress.detail;
42
+ return `${DETAIL_SEPARATOR}${detail}`;
43
+ }
44
+
45
+ /** The line a running turn shows: what the agent is doing, and on what. */
46
+ export function describeActivity(progress: RunProgress | null): string {
47
+ if (progress === null) return `${DEFAULT_ACTIVITY_LABEL}${ACTIVITY_ELLIPSIS}`;
48
+ const label = ACTIVITY_LABELS[progress.activity];
49
+ return `${label}${describeDetail(progress)}${ACTIVITY_ELLIPSIS}`;
50
+ }
51
+
52
+ function sinceReport(progress: RunProgress, nowMs: number): number {
53
+ return Math.max(0, nowMs - progress.receivedAtMs);
54
+ }
55
+
56
+ /** Time since the turn started, counted on from the last report. */
57
+ export function runElapsedMs(
58
+ progress: RunProgress | null,
59
+ nowMs: number,
60
+ ): number {
61
+ if (progress === null) return 0;
62
+ return progress.elapsedMs + sinceReport(progress, nowMs);
63
+ }
64
+
65
+ /** Time since the agent last did anything, counted on from the last report. */
66
+ export function agentQuietMs(
67
+ progress: RunProgress | null,
68
+ nowMs: number,
69
+ ): number {
70
+ if (progress === null) return 0;
71
+ return progress.idleMs + sinceReport(progress, nowMs);
72
+ }
73
+
74
+ /** Whether the agent has been quiet long enough for the chat to say so. */
75
+ export function isAgentQuiet(
76
+ progress: RunProgress | null,
77
+ nowMs: number,
78
+ ): boolean {
79
+ if (progress === null) return false;
80
+ if (SILENT_ACTIVITIES.includes(progress.activity)) return false;
81
+ return agentQuietMs(progress, nowMs) >= QUIET_NOTICE_AFTER_MS;
82
+ }
83
+
84
+ /**
85
+ * Whether the sidecar has said nothing about a running turn, heartbeats
86
+ * included, for so long that the chat can no longer vouch for it.
87
+ */
88
+ export function isRunStalled(lastEventAtMs: number, nowMs: number): boolean {
89
+ return nowMs - lastEventAtMs >= STALL_AFTER_MS;
90
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ MESSAGE_ROLES,
3
+ TODO_STATUS,
4
+ TODO_WRITE_TOOL_NAME,
5
+ } from "../constants/conversation";
6
+ import type {
7
+ ConversationMessage,
8
+ ToolCallMessage,
9
+ } from "../types/conversation";
10
+
11
+ export type TodoItemStatus = "pending" | "in_progress" | "completed";
12
+
13
+ export interface TodoItem {
14
+ content: string;
15
+ status: TodoItemStatus;
16
+ activeForm: string;
17
+ }
18
+
19
+ const TODO_STATUS_VALUES: readonly TodoItemStatus[] = [
20
+ TODO_STATUS.PENDING,
21
+ TODO_STATUS.IN_PROGRESS,
22
+ TODO_STATUS.COMPLETED,
23
+ ];
24
+
25
+ function toStatus(value: unknown): TodoItemStatus {
26
+ return TODO_STATUS_VALUES.includes(value as TodoItemStatus)
27
+ ? (value as TodoItemStatus)
28
+ : TODO_STATUS.PENDING;
29
+ }
30
+
31
+ function toTodo(value: unknown): TodoItem | null {
32
+ if (value === null || typeof value !== "object") return null;
33
+ const record = value as Record<string, unknown>;
34
+ const content = typeof record.content === "string" ? record.content : "";
35
+ if (content.length === 0) return null;
36
+ const activeForm =
37
+ typeof record.activeForm === "string" && record.activeForm.length > 0
38
+ ? record.activeForm
39
+ : content;
40
+ return { content, status: toStatus(record.status), activeForm };
41
+ }
42
+
43
+ export function extractTodos(args: unknown): TodoItem[] {
44
+ if (args === null || typeof args !== "object") return [];
45
+ const todos = (args as Record<string, unknown>).todos;
46
+ if (!Array.isArray(todos)) return [];
47
+ return todos.map(toTodo).filter((todo): todo is TodoItem => todo !== null);
48
+ }
49
+
50
+ export function isTodoWrite(message: ConversationMessage): boolean {
51
+ return (
52
+ message.role === MESSAGE_ROLES.TOOL &&
53
+ message.toolName === TODO_WRITE_TOOL_NAME
54
+ );
55
+ }
56
+
57
+ export function latestTodos(messages: ConversationMessage[]): TodoItem[] {
58
+ for (let i = messages.length - 1; i >= 0; i--) {
59
+ const message = messages[i];
60
+ if (!isTodoWrite(message)) continue;
61
+ return extractTodos((message as ToolCallMessage).args);
62
+ }
63
+ return [];
64
+ }
@@ -0,0 +1,53 @@
1
+ const MAX_DETAIL_CHARS = 80;
2
+ const TRUNCATE_SUFFIX = "...";
3
+
4
+ const TOOL_LABELS: Record<string, string> = {
5
+ Bash: "Run shell command",
6
+ Edit: "Edit file",
7
+ Write: "Write file",
8
+ Read: "Read file",
9
+ Glob: "Glob pattern",
10
+ Grep: "Grep pattern",
11
+ };
12
+
13
+ const DETAIL_ARG_KEYS: Record<string, string> = {
14
+ Bash: "command",
15
+ Edit: "file_path",
16
+ Write: "file_path",
17
+ Read: "file_path",
18
+ Glob: "pattern",
19
+ Grep: "pattern",
20
+ };
21
+
22
+ function shorten(value: string): string {
23
+ if (value.length <= MAX_DETAIL_CHARS) return value;
24
+ const sliceEnd = MAX_DETAIL_CHARS - TRUNCATE_SUFFIX.length;
25
+ return `${value.slice(0, sliceEnd)}${TRUNCATE_SUFFIX}`;
26
+ }
27
+
28
+ function readString(args: unknown, key: string): string {
29
+ if (args === null || typeof args !== "object") return "";
30
+ const value = (args as Record<string, unknown>)[key];
31
+ return typeof value === "string" ? value : "";
32
+ }
33
+
34
+ function detailFor(toolName: string, args: unknown): string {
35
+ const argKey = DETAIL_ARG_KEYS[toolName];
36
+ if (argKey === undefined) return "";
37
+ return shorten(readString(args, argKey));
38
+ }
39
+
40
+ /**
41
+ * Mirror of the sidecar's tool-summary helper: a tool name and its args as a
42
+ * short line for the collapsed cluster summary and the per-tool rows. Kept
43
+ * small: the server already sends a `summary` on permission requests; this
44
+ * only covers streamed tool calls, which carry raw args instead.
45
+ */
46
+ export function toolSummary(toolName: string, args: unknown): string {
47
+ const detail = detailFor(toolName, args);
48
+ return detail.length > 0 ? detail : toolName;
49
+ }
50
+
51
+ export function toolLabel(toolName: string): string {
52
+ return TOOL_LABELS[toolName] ?? toolName;
53
+ }