@duckmind/dm-darwin-x64 0.33.0 → 0.33.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +1 -76
  3. package/package.json +1 -1
  4. package/theme/theme-alps.json +84 -0
  5. package/extensions/dm-chime/README.md +0 -11
  6. package/extensions/dm-chime/docs/protocols.md +0 -107
  7. package/extensions/dm-chime/index.ts +0 -205
  8. package/extensions/dm-chime/package.json +0 -33
  9. package/extensions/dm-phone/README.md +0 -24
  10. package/extensions/dm-phone/index.ts +0 -12
  11. package/extensions/dm-phone/node_modules/.package-lock.json +0 -29
  12. package/extensions/dm-phone/node_modules/ws/LICENSE +0 -20
  13. package/extensions/dm-phone/node_modules/ws/README.md +0 -548
  14. package/extensions/dm-phone/node_modules/ws/browser.js +0 -8
  15. package/extensions/dm-phone/node_modules/ws/index.js +0 -22
  16. package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +0 -131
  17. package/extensions/dm-phone/node_modules/ws/lib/constants.js +0 -19
  18. package/extensions/dm-phone/node_modules/ws/lib/event-target.js +0 -292
  19. package/extensions/dm-phone/node_modules/ws/lib/extension.js +0 -203
  20. package/extensions/dm-phone/node_modules/ws/lib/limiter.js +0 -55
  21. package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +0 -528
  22. package/extensions/dm-phone/node_modules/ws/lib/receiver.js +0 -760
  23. package/extensions/dm-phone/node_modules/ws/lib/sender.js +0 -607
  24. package/extensions/dm-phone/node_modules/ws/lib/stream.js +0 -161
  25. package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +0 -62
  26. package/extensions/dm-phone/node_modules/ws/lib/validation.js +0 -152
  27. package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +0 -562
  28. package/extensions/dm-phone/node_modules/ws/lib/websocket.js +0 -1407
  29. package/extensions/dm-phone/node_modules/ws/package.json +0 -70
  30. package/extensions/dm-phone/node_modules/ws/wrapper.mjs +0 -21
  31. package/extensions/dm-phone/package-lock.json +0 -66
  32. package/extensions/dm-phone/package.json +0 -35
  33. package/extensions/dm-phone/phone-session-pool.ts +0 -8
  34. package/extensions/dm-phone/public/app/attachments.js +0 -233
  35. package/extensions/dm-phone/public/app/autocomplete-controller.js +0 -81
  36. package/extensions/dm-phone/public/app/autocomplete.js +0 -135
  37. package/extensions/dm-phone/public/app/bindings.js +0 -178
  38. package/extensions/dm-phone/public/app/command-catalog.js +0 -76
  39. package/extensions/dm-phone/public/app/commands.js +0 -376
  40. package/extensions/dm-phone/public/app/constants.js +0 -60
  41. package/extensions/dm-phone/public/app/formatters.js +0 -131
  42. package/extensions/dm-phone/public/app/handlers.js +0 -442
  43. package/extensions/dm-phone/public/app/main.js +0 -6
  44. package/extensions/dm-phone/public/app/markdown.js +0 -105
  45. package/extensions/dm-phone/public/app/messages.js +0 -418
  46. package/extensions/dm-phone/public/app/sheet-actions.js +0 -113
  47. package/extensions/dm-phone/public/app/sheet-navigation.js +0 -19
  48. package/extensions/dm-phone/public/app/sheets-view.js +0 -287
  49. package/extensions/dm-phone/public/app/state.js +0 -95
  50. package/extensions/dm-phone/public/app/tool-rendering.js +0 -562
  51. package/extensions/dm-phone/public/app/transport.js +0 -176
  52. package/extensions/dm-phone/public/app/ui.js +0 -417
  53. package/extensions/dm-phone/public/app.js +0 -1
  54. package/extensions/dm-phone/public/icon.svg +0 -15
  55. package/extensions/dm-phone/public/index.html +0 -146
  56. package/extensions/dm-phone/public/manifest.webmanifest +0 -17
  57. package/extensions/dm-phone/public/styles.css +0 -1139
  58. package/extensions/dm-phone/public/sw.js +0 -78
  59. package/extensions/dm-phone/src/extension/duckmind-models.js +0 -264
  60. package/extensions/dm-phone/src/extension/phone-args.ts +0 -121
  61. package/extensions/dm-phone/src/extension/phone-paths.ts +0 -250
  62. package/extensions/dm-phone/src/extension/phone-quota.ts +0 -188
  63. package/extensions/dm-phone/src/extension/phone-runtime.ts +0 -154
  64. package/extensions/dm-phone/src/extension/phone-server-runtime.ts +0 -1217
  65. package/extensions/dm-phone/src/extension/phone-sessions.ts +0 -139
  66. package/extensions/dm-phone/src/extension/phone-static.ts +0 -30
  67. package/extensions/dm-phone/src/extension/phone-tailscale.ts +0 -148
  68. package/extensions/dm-phone/src/extension/phone-theme.ts +0 -85
  69. package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +0 -112
  70. package/extensions/dm-phone/src/extension/register-phone-extension.ts +0 -106
  71. package/extensions/dm-phone/src/extension/types.ts +0 -73
  72. package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +0 -882
  73. package/extensions/dm-phone/src/session-pool/session-pool.ts +0 -470
  74. package/extensions/dm-phone/src/session-pool/session-worker.ts +0 -739
  75. package/extensions/dm-phone/src/session-pool/types.ts +0 -111
  76. package/extensions/dm-phone/src/session-pool/utils.ts +0 -23
  77. package/extensions/dm-phone/test/duckmind-models.test.js +0 -147
  78. package/extensions/dm-thinking-timer/LICENSE +0 -21
  79. package/extensions/dm-thinking-timer/README.md +0 -7
  80. package/extensions/dm-thinking-timer/package.json +0 -20
  81. package/extensions/dm-thinking-timer/thinking-timer.ts +0 -250
@@ -1,111 +0,0 @@
1
- import type { WebSocket } from "ws";
2
-
3
- export type SessionKind = "parent" | "parallel";
4
-
5
- export type SessionSummary = {
6
- id: string;
7
- kind: SessionKind;
8
- sessionId: string | null;
9
- sessionFile: string | null;
10
- sessionName: string | null;
11
- label: string;
12
- secondaryLabel: string;
13
- firstUserPreview: string | null;
14
- lastUserPreview: string | null;
15
- model: {
16
- id: string;
17
- name: string;
18
- provider: string;
19
- actualProvider?: string;
20
- actualModelId?: string;
21
- } | null;
22
- isRunning: boolean;
23
- isStreaming: boolean;
24
- isCompacting: boolean;
25
- messageCount: number;
26
- pendingMessageCount: number;
27
- hasPendingUiRequest: boolean;
28
- lastError: string;
29
- lastActivityAt: number;
30
- childPid: number | null;
31
- cwd?: string | null;
32
- mirrorsCli?: boolean;
33
- };
34
-
35
- export type PendingRequest = {
36
- resolve: (value: any) => void;
37
- reject: (error: Error) => void;
38
- timer: NodeJS.Timeout;
39
- };
40
-
41
- export type PendingClientResponse = {
42
- ws: WebSocket;
43
- responseCommand?: string;
44
- responseData?: Record<string, unknown>;
45
- onSuccess?: (payload: any) => void;
46
- onError?: (payload: any) => void;
47
- };
48
-
49
- export type SessionSnapshot = {
50
- state: any;
51
- messages: any[];
52
- commands: any[];
53
- liveAssistantMessage: any;
54
- liveTools: any[];
55
- };
56
-
57
- export type ClientState = {
58
- activeSessionId: string | null;
59
- };
60
-
61
- export type SessionWorkerOptions<TWorker> = {
62
- cwd: string;
63
- send: (ws: WebSocket, payload: unknown) => void;
64
- onActivity: () => void;
65
- onStateChange: () => void;
66
- onEnvelope: (worker: TWorker, envelope: any) => void;
67
- shouldAutoRestart: (worker: TWorker) => boolean;
68
- };
69
-
70
- export type SessionStatus = {
71
- childRunning: boolean;
72
- cwd: string;
73
- previousCwd: string | null;
74
- isStreaming: boolean;
75
- isCompacting: boolean;
76
- lastError: string;
77
- childPid: number | null;
78
- sessionWorkerId: string;
79
- sessionKind: SessionKind;
80
- };
81
-
82
- export interface SessionController {
83
- id: string;
84
- kind: SessionKind;
85
- cwd: string;
86
- previousCwd: string | null;
87
- currentSessionFile: string | null;
88
- lastError: string;
89
- lastActivityAt: number;
90
- pendingUiRequest: any;
91
- ensureStarted(startOptions?: { sessionFile?: string | null }): Promise<void>;
92
- request(command: Record<string, unknown>, timeoutMs?: number): Promise<any>;
93
- refreshCachedSnapshot(timeoutMs?: number): Promise<SessionSnapshot>;
94
- getSnapshot(): Promise<SessionSnapshot>;
95
- sendClientCommand(command: Record<string, unknown>, meta?: PendingClientResponse): Promise<string | undefined>;
96
- reload(): Promise<void>;
97
- dispose(): Promise<void>;
98
- getStatus(): SessionStatus;
99
- getSummary(): SessionSummary;
100
- getCachedSnapshot(): SessionSnapshot;
101
- setTrackedCwd?(cwd: string, previousCwd?: string | null): void;
102
- }
103
-
104
- export type PhoneSessionPoolOptions = {
105
- cwd: string;
106
- send: (ws: WebSocket, payload: unknown) => void;
107
- onActivity: () => void;
108
- buildStatusMeta: () => Record<string, unknown>;
109
- createDefaultSession: () => SessionController;
110
- createParallelSession: (sessionFile?: string | null) => SessionController;
111
- };
@@ -1,23 +0,0 @@
1
- export function contentToPreviewText(content: unknown): string {
2
- if (typeof content === "string") {
3
- return content.replace(/\s+/g, " ").trim();
4
- }
5
-
6
- if (!Array.isArray(content)) {
7
- return "";
8
- }
9
-
10
- return content
11
- .map((part: any) => {
12
- if (part?.type === "text") return part.text || "";
13
- if (part?.type === "image") return "[image]";
14
- return "";
15
- })
16
- .join(" ")
17
- .replace(/\s+/g, " ")
18
- .trim();
19
- }
20
-
21
- export function shortId(value: unknown): string {
22
- return String(value || "").trim().slice(0, 8);
23
- }
@@ -1,147 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
- import {
7
- getPhoneVisibleModels,
8
- normalizePhoneSessionModel,
9
- resolvePhoneVisibleModel,
10
- } from "../src/extension/duckmind-models.js";
11
-
12
- const BASE_MODELS = [
13
- { provider: "openrouter", id: "@preset/free", name: "OpenRouter Free" },
14
- { provider: "openrouter", id: "@preset/auto", name: "OpenRouter Auto" },
15
- { provider: "openrouter", id: "@preset/lite", name: "OpenRouter Lite" },
16
- { provider: "openrouter", id: "@preset/smart", name: "OpenRouter Smart" },
17
- { provider: "openrouter", id: "@preset/deep", name: "OpenRouter Deep" },
18
- { provider: "openrouter", id: "@preset/image2", name: "OpenRouter Image2" },
19
- { provider: "openrouter", id: "openai/gpt-4o-mini", name: "GPT-4o Mini" },
20
- { provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" },
21
- { provider: "openai-codex", id: "gpt-5.5", name: "GPT-5.5" },
22
- ];
23
-
24
- function withTempAgentDir({ withUltradexAccounts = false } = {}, fn) {
25
- const tempDir = mkdtempSync(join(tmpdir(), "dm-phone-models-"));
26
- const previousAgentDir = process.env.DM_CODING_AGENT_DIR;
27
-
28
- try {
29
- process.env.DM_CODING_AGENT_DIR = tempDir;
30
- if (withUltradexAccounts) {
31
- writeFileSync(join(tempDir, "codex-accounts.json"), JSON.stringify({ accounts: [{ id: "acct-1" }] }), "utf-8");
32
- }
33
- return fn(tempDir);
34
- } finally {
35
- if (previousAgentDir === undefined) {
36
- delete process.env.DM_CODING_AGENT_DIR;
37
- } else {
38
- process.env.DM_CODING_AGENT_DIR = previousAgentDir;
39
- }
40
- rmSync(tempDir, { recursive: true, force: true });
41
- }
42
- }
43
-
44
- test("phone model picker keeps DuckMind aliases and appends connected provider models", () => {
45
- withTempAgentDir({}, () => {
46
- const visible = getPhoneVisibleModels(BASE_MODELS);
47
- const refs = visible.map((model) => `${model.provider}/${model.id}`);
48
- assert.deepEqual(refs.slice(0, 6), [
49
- "duckmind/free",
50
- "duckmind/auto",
51
- "duckmind/lite",
52
- "duckmind/smart",
53
- "duckmind/deep",
54
- "duckmind/image2",
55
- ]);
56
- assert.deepEqual(visible.slice(0, 6).map((model) => model.name), [
57
- "Free",
58
- "Auto",
59
- "Lite",
60
- "Smart",
61
- "Deep",
62
- "Image2",
63
- ]);
64
- assert.ok(refs.includes("duckmind/openai/gpt-4o-mini"));
65
- assert.ok(!refs.includes("openrouter/openai/gpt-4o-mini"));
66
- assert.equal(visible.find((model) => model.id === "openai/gpt-4o-mini")?.actualProvider, "openrouter");
67
- assert.ok(refs.includes("anthropic/claude-sonnet-4-5"));
68
- assert.ok(refs.includes("openai-codex/gpt-5.5"));
69
- assert.ok(!refs.includes("duckmind/ultra"));
70
- });
71
- });
72
-
73
- test("phone model picker adds ultra only when ultradex accounts are present", () => {
74
- withTempAgentDir({ withUltradexAccounts: true }, () => {
75
- const visible = getPhoneVisibleModels(BASE_MODELS);
76
- assert.deepEqual(visible.map((model) => `${model.provider}/${model.id}`).slice(0, 7), [
77
- "duckmind/free",
78
- "duckmind/auto",
79
- "duckmind/lite",
80
- "duckmind/smart",
81
- "duckmind/deep",
82
- "duckmind/image2",
83
- "duckmind/ultra",
84
- ]);
85
- });
86
- });
87
-
88
- test("duckmind aliases resolve back to their actual runtime model", () => {
89
- withTempAgentDir({}, () => {
90
- const resolved = resolvePhoneVisibleModel(BASE_MODELS, "duckmind", "smart");
91
- assert.ok(resolved);
92
- assert.equal(resolved.option.provider, "duckmind");
93
- assert.equal(resolved.option.id, "smart");
94
- assert.equal(resolved.option.name, "Smart");
95
- assert.equal(resolved.model.provider, "openrouter");
96
- assert.equal(resolved.model.id, "@preset/smart");
97
- });
98
- });
99
-
100
- test("providerless actual model ids still map back to the duckmind alias for compat", () => {
101
- withTempAgentDir({}, () => {
102
- const resolved = resolvePhoneVisibleModel(BASE_MODELS, "", "@preset/lite");
103
- assert.ok(resolved);
104
- assert.equal(resolved.option.id, "lite");
105
- assert.equal(resolved.option.actualModelId, "@preset/lite");
106
- });
107
- });
108
-
109
- test("session model snapshots normalize raw OpenRouter models to DuckMind labels", () => {
110
- withTempAgentDir({}, () => {
111
- assert.deepEqual(
112
- normalizePhoneSessionModel({
113
- provider: "openrouter",
114
- id: "@preset/deep",
115
- name: "OpenRouter Deep",
116
- contextWindow: 1234,
117
- }),
118
- {
119
- provider: "duckmind",
120
- id: "deep",
121
- name: "Deep",
122
- actualProvider: "openrouter",
123
- actualModelId: "@preset/deep",
124
- contextWindow: 1234,
125
- },
126
- );
127
- });
128
- });
129
-
130
- test("session model snapshots normalize raw Codex ultra models to DuckMind Ultra", () => {
131
- withTempAgentDir({ withUltradexAccounts: true }, () => {
132
- assert.deepEqual(
133
- normalizePhoneSessionModel({
134
- provider: "openai-codex",
135
- id: "gpt-5.5",
136
- name: "GPT-5.5",
137
- }),
138
- {
139
- provider: "duckmind",
140
- id: "ultra",
141
- name: "Ultra",
142
- actualProvider: "openai-codex",
143
- actualModelId: "gpt-5.5",
144
- },
145
- );
146
- });
147
- });
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 xryul
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,7 +0,0 @@
1
- # dm-thinking-timer
2
-
3
- Bundled DM extension that shows a live timer next to collapsed Thinking blocks.
4
-
5
- ## Bundled with dm
6
-
7
- No separate install step is required. Install `dm` and restart the TUI if the timer patch is not already active.
@@ -1,20 +0,0 @@
1
- {
2
- "name": "dm-thinking-timer",
3
- "version": "0.1.4",
4
- "description": "DM extension that shows a live timer next to collapsed Thinking blocks",
5
- "type": "module",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/xRyul/pi-thinking-timer.git"
10
- },
11
- "peerDependencies": {
12
- "@mariozechner/pi-coding-agent": "*",
13
- "@mariozechner/pi-tui": "*"
14
- },
15
- "pi": {
16
- "extensions": [
17
- "./thinking-timer.ts"
18
- ]
19
- }
20
- }
@@ -1,250 +0,0 @@
1
- /**
2
- * Thinking Timer Extension
3
- *
4
- * Goal: show a live ticking timer *inline* on the collapsed "Thinking..." line,
5
- * so you see:
6
- *
7
- * Thinking... 6.5s
8
- *
9
- * instead of having a second "Working..."/"Thinking ..." indicator line.
10
- *
11
- * Implementation notes:
12
- * - We track thinking_start/thinking_end stream events to measure durations.
13
- * - We monkey-patch AssistantMessageComponent.updateContent() to replace the
14
- * hardcoded "Thinking..." label with "Thinking... <time>".
15
- * - This relies on internal rendering behavior (but uses exported components),
16
- * so it may break if dm changes how it renders collapsed thinking blocks.
17
- */
18
-
19
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
20
- import { AssistantMessageComponent } from "@mariozechner/pi-coding-agent";
21
- import { Text } from "@mariozechner/pi-tui";
22
-
23
- type Store = {
24
- /** Active thinking blocks: key -> start time (ms since epoch) */
25
- starts: Map<string, number>;
26
- /** Finalized thinking blocks: key -> duration ms */
27
- durations: Map<string, number>;
28
- /** Rendered label components for collapsed thinking blocks */
29
- labels: Map<string, Text>;
30
- /** Latest theme reference (ctx.ui.theme) */
31
- theme?: ExtensionContext["ui"]["theme"];
32
- };
33
-
34
- const STORE_KEY = Symbol.for("dm.extensions.thinkingTimer.store");
35
- const PATCH_KEY = Symbol.for("dm.extensions.thinkingTimer.patch");
36
-
37
- function getStore(): Store | undefined {
38
- return (globalThis as any)[STORE_KEY] as Store | undefined;
39
- }
40
-
41
- function formatElapsed(ms: number): string {
42
- const totalSeconds = ms / 1000;
43
- if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s`;
44
- const minutes = Math.floor(totalSeconds / 60);
45
- const seconds = totalSeconds - minutes * 60;
46
- return `${minutes}:${seconds.toFixed(1).padStart(4, "0")}`;
47
- }
48
-
49
- function makeThinkingLabel(theme: Store["theme"] | undefined, ms: number | null): string {
50
- if (!theme) {
51
- return ms === null ? "Thinking..." : `Thinking... ${formatElapsed(ms)}`;
52
- }
53
- if (ms === null) {
54
- return theme.italic(theme.fg("thinkingText", "Thinking..."));
55
- }
56
- const base = theme.fg("thinkingText", "Thinking...");
57
- const time = theme.fg("dim", ` ${formatElapsed(ms)}`);
58
- return theme.italic(base + time);
59
- }
60
-
61
- function keyFor(timestamp: number, contentIndex: number): string {
62
- return `${timestamp}:${contentIndex}`;
63
- }
64
-
65
- function ensureAssistantMessagePatchInstalled(): void {
66
- const proto: any = AssistantMessageComponent.prototype as any;
67
- if (proto[PATCH_KEY]) return;
68
- proto[PATCH_KEY] = true;
69
-
70
- const originalUpdateContent = proto.updateContent;
71
-
72
- proto.updateContent = function patchedUpdateContent(this: any, message: any) {
73
- originalUpdateContent.call(this, message);
74
-
75
- try {
76
- const store = getStore();
77
- if (!store) return;
78
- if (!message || !message.content || !Array.isArray(message.content)) return;
79
- if (!this.hideThinkingBlock) return;
80
- if (!this.contentContainer || !Array.isArray(this.contentContainer.children)) return;
81
-
82
- // Find thinking content indices that would produce a collapsed label.
83
- const thinkingIndices: number[] = [];
84
- for (let i = 0; i < message.content.length; i++) {
85
- const c = message.content[i];
86
- if (c?.type === "thinking" && typeof c.thinking === "string" && c.thinking.trim()) {
87
- thinkingIndices.push(i);
88
- }
89
- }
90
- if (thinkingIndices.length === 0) return;
91
-
92
- // Find the Text components that currently contain the hardcoded "Thinking..." label.
93
- const labelComponents: Text[] = [];
94
- for (const child of this.contentContainer.children as any[]) {
95
- // Be defensive: avoid relying on instanceof across module boundaries.
96
- if (!child || typeof child !== "object") continue;
97
- if (typeof child.setText !== "function") continue;
98
- if (typeof child.text !== "string") continue;
99
- if (!child.text.includes("Thinking...")) continue;
100
- labelComponents.push(child as Text);
101
- }
102
- if (labelComponents.length === 0) return;
103
-
104
- const count = Math.min(thinkingIndices.length, labelComponents.length);
105
- for (let j = 0; j < count; j++) {
106
- const contentIndex = thinkingIndices[j]!;
107
- const label = labelComponents[j]!;
108
- const k = keyFor(message.timestamp, contentIndex);
109
- store.labels.set(k, label);
110
-
111
- // Apply either live or finalized duration if we have it.
112
- let ms: number | null = null;
113
- const start = store.starts.get(k);
114
- const dur = store.durations.get(k);
115
- if (dur !== undefined) {
116
- ms = dur;
117
- } else if (start !== undefined) {
118
- ms = Date.now() - start;
119
- }
120
-
121
- // Only override label when we have timing info (or when live),
122
- // otherwise leave the original rendering alone.
123
- if (ms !== null) {
124
- label.setText(makeThinkingLabel(store.theme, ms));
125
- }
126
- }
127
- } catch {
128
- // Never break rendering
129
- }
130
- };
131
- }
132
-
133
- export default function (pi: ExtensionAPI) {
134
- // Shared store used by the patch (global so /reload replaces it cleanly)
135
- const store: Store = {
136
- starts: new Map(),
137
- durations: new Map(),
138
- labels: new Map(),
139
- theme: undefined,
140
- };
141
- (globalThis as any)[STORE_KEY] = store;
142
- ensureAssistantMessagePatchInstalled();
143
-
144
- let ticker: ReturnType<typeof setInterval> | null = null;
145
-
146
- function stopTicker() {
147
- if (ticker) {
148
- clearInterval(ticker);
149
- ticker = null;
150
- }
151
- }
152
-
153
- function tick() {
154
- const s = getStore();
155
- if (!s) return;
156
- if (s.starts.size === 0) {
157
- stopTicker();
158
- return;
159
- }
160
- for (const [k, start] of s.starts.entries()) {
161
- const label = s.labels.get(k);
162
- if (!label) continue;
163
- label.setText(makeThinkingLabel(s.theme, Date.now() - start));
164
- }
165
- }
166
-
167
- function startTicker() {
168
- if (ticker) return;
169
- ticker = setInterval(tick, 100);
170
- }
171
-
172
- function finalizeThinkingBlock(k: string, endTimeMs = Date.now()) {
173
- const s = getStore();
174
- if (!s) return;
175
- const start = s.starts.get(k);
176
- if (start === undefined) return;
177
- const dur = Math.max(0, endTimeMs - start);
178
- s.starts.delete(k);
179
- s.durations.set(k, dur);
180
-
181
- const label = s.labels.get(k);
182
- if (label) {
183
- label.setText(makeThinkingLabel(s.theme, dur));
184
- }
185
- }
186
-
187
- function resetAll(ctx: ExtensionContext) {
188
- stopTicker();
189
- store.starts.clear();
190
- store.durations.clear();
191
- store.labels.clear();
192
- store.theme = ctx.ui.theme;
193
- // Ensure we don't leave a custom working message around from earlier versions.
194
- ctx.ui.setWorkingMessage();
195
- }
196
-
197
- pi.on("session_start", async (_event, ctx) => {
198
- resetAll(ctx);
199
- });
200
-
201
- pi.on("message_update", async (event, ctx) => {
202
- store.theme = ctx.ui.theme;
203
-
204
- const se = event.assistantMessageEvent as any;
205
- if (!se || typeof se.type !== "string") return;
206
-
207
- if (se.type === "thinking_start" || se.type === "thinking_delta") {
208
- const msg = se.partial;
209
- const k = keyFor(msg.timestamp, se.contentIndex);
210
- if (!store.starts.has(k)) {
211
- store.starts.set(k, Date.now());
212
- }
213
- startTicker();
214
- // Try immediate paint if label already exists
215
- tick();
216
- return;
217
- }
218
-
219
- if (se.type === "thinking_end") {
220
- const msg = se.partial;
221
- const k = keyFor(msg.timestamp, se.contentIndex);
222
- finalizeThinkingBlock(k);
223
- if (store.starts.size === 0) stopTicker();
224
- return;
225
- }
226
- });
227
-
228
- // Safety: if a message ends while a thinking_start was seen but thinking_end was not,
229
- // finalize any active thinking blocks for that message.
230
- pi.on("message_end", async (event, ctx) => {
231
- store.theme = ctx.ui.theme;
232
- const msg: any = event.message;
233
- if (!msg || msg.role !== "assistant" || !Array.isArray(msg.content)) return;
234
-
235
- for (let i = 0; i < msg.content.length; i++) {
236
- const c = msg.content[i];
237
- if (c?.type !== "thinking") continue;
238
- const k = keyFor(msg.timestamp, i);
239
- if (store.starts.has(k)) {
240
- finalizeThinkingBlock(k, Date.now());
241
- }
242
- }
243
- if (store.starts.size === 0) stopTicker();
244
- });
245
-
246
-
247
- pi.on("session_shutdown", async (_event, ctx) => {
248
- resetAll(ctx);
249
- });
250
- }