@duckmind/dm-darwin-arm64 0.33.1 → 0.33.4

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.
@@ -1,340 +0,0 @@
1
- import {
2
- buildSessionEntries,
3
- createMockCtx,
4
- createMockDM,
5
- makeAssistantMessage,
6
- makeUserMessage,
7
- } from "@juicesharp/rpiv-test-utils";
8
- import { beforeEach, describe, expect, it, vi } from "vitest";
9
-
10
- vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
11
- const actual = await importOriginal<typeof import("@mariozechner/pi-ai")>();
12
- return {
13
- ...actual,
14
- completeSimple: vi.fn(),
15
- getSupportedThinkingLevels: vi.fn(() => ["off", "minimal", "low", "medium", "high"]),
16
- };
17
- });
18
-
19
- import { type AssistantMessage, completeSimple, type UserMessage } from "@mariozechner/pi-ai";
20
- import {
21
- assistantMessageText,
22
- ASK_STATE_KEY,
23
- ASK_SYSTEM_PROMPT,
24
- CROSS_SESSION_HINT_LIMIT,
25
- clearSessionHistory,
26
- executeAsk,
27
- invalidateSnapshot,
28
- registerAskCommand,
29
- registerInvalidationHooks,
30
- registerMessageEndSnapshot,
31
- userMessageText,
32
- } from "./ask.js";
33
-
34
- function makeCompletionResponse(input: {
35
- text?: string;
36
- stopReason?: "done" | "aborted" | "error" | "toolUse";
37
- errorMessage?: string;
38
- }): AssistantMessage {
39
- return {
40
- role: "assistant",
41
- content: input.text ? [{ type: "text", text: input.text }] : [],
42
- timestamp: Date.now(),
43
- stopReason: input.stopReason ?? "done",
44
- errorMessage: input.errorMessage,
45
- } as unknown as AssistantMessage;
46
- }
47
-
48
- beforeEach(() => {
49
- vi.mocked(completeSimple).mockReset();
50
- delete (globalThis as Record<symbol, unknown>)[ASK_STATE_KEY];
51
- });
52
-
53
- describe("userMessageText", () => {
54
- it("returns string content as-is", () => {
55
- const msg = { role: "user", content: "hi", timestamp: 0 } as unknown as UserMessage;
56
- expect(userMessageText(msg)).toBe("hi");
57
- });
58
- it("joins text parts from array content", () => {
59
- expect(
60
- userMessageText({
61
- role: "user",
62
- content: [
63
- { type: "text", text: "a" },
64
- { type: "text", text: "b" },
65
- ],
66
- timestamp: 0,
67
- } as unknown as UserMessage),
68
- ).toBe("a\nb");
69
- });
70
- it("ignores non-text parts", () => {
71
- expect(
72
- userMessageText({
73
- role: "user",
74
- content: [
75
- { type: "text", text: "a" },
76
- { type: "image", data: "..." } as unknown as { type: "text"; text: string },
77
- ],
78
- timestamp: 0,
79
- } as unknown as UserMessage),
80
- ).toBe("a");
81
- });
82
- });
83
-
84
- describe("assistantMessageText", () => {
85
- it("joins text parts only, skips toolCalls", () => {
86
- const msg = makeAssistantMessage({
87
- text: "hello",
88
- toolCalls: [{ id: "c1", name: "web_search", arguments: {} }],
89
- });
90
- expect(assistantMessageText(msg)).toBe("hello");
91
- });
92
- it("returns empty string for content without text parts", () => {
93
- const msg = makeAssistantMessage({
94
- toolCalls: [{ id: "c1", name: "t", arguments: {} }],
95
- });
96
- expect(assistantMessageText(msg)).toBe("");
97
- });
98
- });
99
-
100
- describe("ASK_SYSTEM_PROMPT + ASK_STATE_KEY + CROSS_SESSION_HINT_LIMIT", () => {
101
- it("ASK_SYSTEM_PROMPT is a non-empty string loaded from prompts dir", () => {
102
- expect(typeof ASK_SYSTEM_PROMPT).toBe("string");
103
- expect(ASK_SYSTEM_PROMPT.length).toBeGreaterThan(0);
104
- });
105
- it("ASK_STATE_KEY is the shared Symbol.for('dm-ask')", () => {
106
- expect(ASK_STATE_KEY).toBe(Symbol.for("dm-ask"));
107
- });
108
- it("CROSS_SESSION_HINT_LIMIT is 10", () => {
109
- expect(CROSS_SESSION_HINT_LIMIT).toBe(10);
110
- });
111
- });
112
-
113
- describe("clearSessionHistory + invalidateSnapshot", () => {
114
- it("clearSessionHistory resets per-session history list", async () => {
115
- const ctx = createMockCtx();
116
- vi.mocked(completeSimple).mockResolvedValueOnce(makeCompletionResponse({ text: "answer" }) as never);
117
- ctx.model = { provider: "anthropic", id: "sonnet-4.6" } as never;
118
- await executeAsk("q", ctx, new AbortController());
119
- clearSessionHistory(ctx);
120
- const state = (globalThis as Record<symbol, { histories: Map<string, unknown[]> }>)[ASK_STATE_KEY];
121
- expect(state.histories.get("/tmp/test-session.jsonl")).toEqual([]);
122
- });
123
- it("invalidateSnapshot deletes the session's snapshot entry", () => {
124
- const ctx = createMockCtx();
125
- (globalThis as Record<symbol, { snapshots: Map<string, unknown> }>)[ASK_STATE_KEY] = {
126
- histories: new Map(),
127
- snapshots: new Map([["/tmp/test-session.jsonl", { messages: [] }]]),
128
- } as never;
129
- invalidateSnapshot(ctx);
130
- const state = (globalThis as Record<symbol, { snapshots: Map<string, unknown> }>)[ASK_STATE_KEY];
131
- expect(state.snapshots.has("/tmp/test-session.jsonl")).toBe(false);
132
- });
133
- });
134
-
135
- describe("executeAsk — ok path", () => {
136
- it("returns ok=true with answer + userMessage + assistantMessage", async () => {
137
- const ctx = createMockCtx();
138
- ctx.model = { provider: "a", id: "m" } as never;
139
- vi.mocked(completeSimple).mockResolvedValueOnce(makeCompletionResponse({ text: "answer text" }) as never);
140
- const r = await executeAsk("question", ctx, new AbortController());
141
- expect(r.ok).toBe(true);
142
- expect(r.answer).toBe("answer text");
143
- expect(r.userMessage?.content).toEqual([{ type: "text", text: "question" }]);
144
- expect(r.assistantMessage).toBeDefined();
145
- });
146
- });
147
-
148
- describe("executeAsk — error branches", () => {
149
- it("returns error when no model", async () => {
150
- const ctx = createMockCtx();
151
- ctx.model = undefined;
152
- const r = await executeAsk("q", ctx, new AbortController());
153
- expect(r).toMatchObject({ ok: false, error: "/ask requires an active model" });
154
- });
155
- it("returns error when getApiKeyAndHeaders is not ok", async () => {
156
- const ctx = createMockCtx();
157
- ctx.model = { provider: "a", id: "m" } as never;
158
- ctx.modelRegistry = {
159
- ...ctx.modelRegistry,
160
- getApiKeyAndHeaders: vi.fn(async () => ({ ok: false, error: "bad creds" })),
161
- } as never;
162
- const r = await executeAsk("q", ctx, new AbortController());
163
- expect(r.ok).toBe(false);
164
- expect(r.error).toContain("misconfigured");
165
- expect(r.error).toContain("bad creds");
166
- });
167
- it("returns error when apiKey absent", async () => {
168
- const ctx = createMockCtx();
169
- ctx.model = { provider: "a", id: "m" } as never;
170
- ctx.modelRegistry = {
171
- ...ctx.modelRegistry,
172
- getApiKeyAndHeaders: vi.fn(async () => ({ ok: true, apiKey: "", headers: {} })),
173
- } as never;
174
- const r = await executeAsk("q", ctx, new AbortController());
175
- expect(r.ok).toBe(false);
176
- expect(r.error).toContain("no API key");
177
- });
178
- it("returns aborted when stopReason=aborted", async () => {
179
- const ctx = createMockCtx();
180
- ctx.model = { provider: "a", id: "m" } as never;
181
- vi.mocked(completeSimple).mockResolvedValueOnce(makeCompletionResponse({ stopReason: "aborted" }) as never);
182
- const r = await executeAsk("q", ctx, new AbortController());
183
- expect(r).toMatchObject({ ok: false, aborted: true });
184
- });
185
- it("returns error when stopReason=error", async () => {
186
- const ctx = createMockCtx();
187
- ctx.model = { provider: "a", id: "m" } as never;
188
- vi.mocked(completeSimple).mockResolvedValueOnce(
189
- makeCompletionResponse({ stopReason: "error", errorMessage: "remote 500" }) as never,
190
- );
191
- const r = await executeAsk("q", ctx, new AbortController());
192
- expect(r.ok).toBe(false);
193
- expect(r.error).toContain("remote 500");
194
- });
195
- it("returns error when response has no text content", async () => {
196
- const ctx = createMockCtx();
197
- ctx.model = { provider: "a", id: "m" } as never;
198
- vi.mocked(completeSimple).mockResolvedValueOnce(makeCompletionResponse({ stopReason: "done" }) as never);
199
- const r = await executeAsk("q", ctx, new AbortController());
200
- expect(r.ok).toBe(false);
201
- expect(r.error).toContain("no text content");
202
- });
203
- it("translates controller.signal.aborted on thrown error to aborted=true", async () => {
204
- const ctx = createMockCtx();
205
- ctx.model = { provider: "a", id: "m" } as never;
206
- const controller = new AbortController();
207
- controller.abort();
208
- vi.mocked(completeSimple).mockRejectedValueOnce(new Error("abort"));
209
- const r = await executeAsk("q", ctx, controller);
210
- expect(r).toMatchObject({ ok: false, aborted: true });
211
- });
212
- it("wraps unknown throws as errCallThrew", async () => {
213
- const ctx = createMockCtx();
214
- ctx.model = { provider: "a", id: "m" } as never;
215
- vi.mocked(completeSimple).mockRejectedValueOnce(new Error("boom"));
216
- const r = await executeAsk("q", ctx, new AbortController());
217
- expect(r.ok).toBe(false);
218
- expect(r.error).toContain("call threw");
219
- expect(r.error).toContain("boom");
220
- });
221
- });
222
-
223
- describe("executeAsk — cross-session hint", () => {
224
- it("appends cross-session question list to systemPrompt", async () => {
225
- const ctx = createMockCtx();
226
- ctx.model = { provider: "a", id: "m" } as never;
227
-
228
- vi.mocked(completeSimple).mockResolvedValueOnce(makeCompletionResponse({ text: "first" }) as never);
229
- await executeAsk("first-question", ctx, new AbortController());
230
-
231
- vi.mocked(completeSimple).mockImplementationOnce((async (_model: unknown, req: { systemPrompt: string }) => {
232
- expect(req.systemPrompt).toContain("## Recent /ask questions across sessions");
233
- expect(req.systemPrompt).toContain("first-question");
234
- return makeCompletionResponse({ text: "second" });
235
- }) as never);
236
- await executeAsk("second-question", ctx, new AbortController());
237
- });
238
- it("caps cross-session list to CROSS_SESSION_HINT_LIMIT=10", async () => {
239
- const ctx = createMockCtx();
240
- ctx.model = { provider: "a", id: "m" } as never;
241
- for (let i = 0; i < 12; i++) {
242
- vi.mocked(completeSimple).mockResolvedValueOnce(makeCompletionResponse({ text: `a${i}` }) as never);
243
- await executeAsk(`q${i}`, ctx, new AbortController());
244
- }
245
- vi.mocked(completeSimple).mockImplementationOnce((async (_m: unknown, req: { systemPrompt: string }) => {
246
- const lines = req.systemPrompt.match(/^\d+\. /gm) ?? [];
247
- expect(lines.length).toBe(10);
248
- expect(req.systemPrompt).toContain("q11");
249
- expect(req.systemPrompt).not.toContain("q0.");
250
- return makeCompletionResponse({ text: "ok" });
251
- }) as never);
252
- await executeAsk("final", ctx, new AbortController());
253
- });
254
- });
255
-
256
- describe("executeAsk — branch threading", () => {
257
- it("prepends live branch messages when no snapshot exists", async () => {
258
- const ctx = createMockCtx({
259
- branch: buildSessionEntries([makeUserMessage("earlier user turn")]),
260
- });
261
- ctx.model = { provider: "a", id: "m" } as never;
262
- vi.mocked(completeSimple).mockImplementationOnce((async (_m: unknown, req: { messages: unknown[] }) => {
263
- expect(req.messages[0]).toMatchObject({
264
- role: "user",
265
- content: [{ type: "text", text: "earlier user turn" }],
266
- });
267
- return makeCompletionResponse({ text: "ok" });
268
- }) as never);
269
- await executeAsk("q", ctx, new AbortController());
270
- });
271
- });
272
-
273
- describe("registerMessageEndSnapshot", () => {
274
- it("writes a snapshot on non-toolUse assistant message_end", async () => {
275
- const { DM, captured } = createMockDM();
276
- registerMessageEndSnapshot(pi);
277
- const handler = captured.events.get("message_end")?.[0];
278
- expect(handler).toBeDefined();
279
- const ctx = createMockCtx({
280
- branch: buildSessionEntries([makeUserMessage("u1"), makeAssistantMessage({ text: "a1" })]),
281
- });
282
- await handler?.({ message: makeAssistantMessage({ text: "a1" }) } as never, ctx as never);
283
- const state = (globalThis as Record<symbol, { snapshots: Map<string, unknown> }>)[ASK_STATE_KEY];
284
- expect(state.snapshots.has("/tmp/test-session.jsonl")).toBe(true);
285
- });
286
- it("skips snapshot when stopReason=toolUse", async () => {
287
- const { DM, captured } = createMockDM();
288
- registerMessageEndSnapshot(pi);
289
- const handler = captured.events.get("message_end")?.[0];
290
- const msg = { ...makeAssistantMessage({ text: "x" }), stopReason: "toolUse" };
291
- const ctx = createMockCtx();
292
- await handler?.({ message: msg } as never, ctx as never);
293
- const state = (globalThis as unknown as Record<symbol, { snapshots?: Map<string, unknown> } | undefined>)[
294
- ASK_STATE_KEY
295
- ];
296
- expect(state?.snapshots?.has("/tmp/test-session.jsonl") ?? false).toBe(false);
297
- });
298
- it("skips snapshot for user role", async () => {
299
- const { DM, captured } = createMockDM();
300
- registerMessageEndSnapshot(pi);
301
- const handler = captured.events.get("message_end")?.[0];
302
- await handler?.({ message: makeUserMessage("u") } as never, createMockCtx() as never);
303
- const state = (globalThis as unknown as Record<symbol, { snapshots?: Map<string, unknown> } | undefined>)[
304
- ASK_STATE_KEY
305
- ];
306
- expect(state?.snapshots?.has("/tmp/test-session.jsonl") ?? false).toBe(false);
307
- });
308
- });
309
-
310
- describe("registerInvalidationHooks", () => {
311
- it("wires session_compact + session_tree", () => {
312
- const { DM, captured } = createMockDM();
313
- registerInvalidationHooks(pi);
314
- expect(captured.events.has("session_compact")).toBe(true);
315
- expect(captured.events.has("session_tree")).toBe(true);
316
- });
317
- it("handlers clear the snapshot for the session", async () => {
318
- const { DM, captured } = createMockDM();
319
- registerInvalidationHooks(pi);
320
- (globalThis as Record<symbol, { snapshots: Map<string, unknown> }>)[ASK_STATE_KEY] = {
321
- histories: new Map(),
322
- snapshots: new Map([["/tmp/test-session.jsonl", { messages: [] }]]),
323
- } as never;
324
- const compactHandler = captured.events.get("session_compact")?.[0];
325
- await compactHandler?.({} as never, createMockCtx() as never);
326
- const state = (globalThis as Record<symbol, { snapshots: Map<string, unknown> }>)[ASK_STATE_KEY];
327
- expect(state.snapshots.has("/tmp/test-session.jsonl")).toBe(false);
328
- });
329
- });
330
-
331
- describe("registerAskCommand", () => {
332
- it("registers /ask with handler", () => {
333
- const { DM, captured } = createMockDM();
334
- registerAskCommand(pi);
335
- expect(captured.commands.has("ask")).toBe(true);
336
- const cmd = captured.commands.get("ask");
337
- expect(cmd?.description).toContain("side question");
338
- expect(typeof cmd?.handler).toBe("function");
339
- });
340
- });
Binary file
@@ -1,70 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 800" role="img" aria-label="dm-ask — Side questions without polluting the main conversation" preserveAspectRatio="xMidYMid meet">
2
- <title>dm-ask — Side questions without polluting the main conversation</title>
3
- <defs>
4
- <style>
5
- .mono { font-family: 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', ui-monospace, Menlo, Consolas, monospace; }
6
- .ink { fill: #F4EFE6; }
7
- .muted { fill: #7A7468; }
8
- .dim { fill: #4A463F; }
9
- .accent { fill: #FF7A3D; }
10
- .mint { fill: #6FD0B8; }
11
- </style>
12
- <filter id="grain" x="0" y="0" width="100%" height="100%">
13
- <feTurbulence type="fractalNoise" baseFrequency="1.4" numOctaves="2" seed="19"/>
14
- <feColorMatrix values="0 0 0 0 0.96 0 0 0 0 0.94 0 0 0 0 0.88 0 0 0 0.04 0"/>
15
- </filter>
16
- <radialGradient id="vignette" cx="50%" cy="50%" r="70%">
17
- <stop offset="55%" stop-color="#070605" stop-opacity="0"/>
18
- <stop offset="100%" stop-color="#000" stop-opacity="0.7"/>
19
- </radialGradient>
20
- <filter id="winShadow" x="-10%" y="-10%" width="120%" height="130%">
21
- <feDropShadow dx="0" dy="14" stdDeviation="22" flood-color="#000" flood-opacity="0.6"/>
22
- </filter>
23
- <clipPath id="wclip"><rect x="64" y="56" width="1152" height="688" rx="12"/></clipPath>
24
- </defs>
25
-
26
- <rect width="1280" height="800" fill="#070605"/>
27
- <rect width="1280" height="800" filter="url(#grain)" opacity="0.45"/>
28
- <rect width="1280" height="800" fill="url(#vignette)"/>
29
-
30
- <rect x="64" y="56" width="1152" height="688" rx="12" fill="#0E0D0B" filter="url(#winShadow)"/>
31
-
32
- <g clip-path="url(#wclip)">
33
- <rect x="64" y="56" width="1152" height="44" fill="#16120E"/>
34
- <line x1="64" y1="100" x2="1216" y2="100" stroke="#2A2620"/>
35
- <circle cx="92" cy="78" r="7" fill="#FF5F57"/>
36
- <circle cx="120" cy="78" r="7" fill="#FFBD2E"/>
37
- <circle cx="148" cy="78" r="7" fill="#28C840"/>
38
- <text class="mono muted" x="640" y="84" font-size="14" letter-spacing="1.4" text-anchor="middle">~/rpiv-mono &#8212; DM &#8212; dm-ask</text>
39
-
40
- <text class="mono mint" x="120" y="184" font-size="46" font-weight="700"># dm-ask</text>
41
- <text class="mono muted" x="120" y="222" font-size="20" letter-spacing="0.5">Side question. Zero pollution.</text>
42
-
43
- <g opacity="0.6">
44
- <text class="mono" x="120" y="296" font-size="20"><tspan class="muted">user &#8250;</tspan> <tspan class="muted">let&#8217;s migrate the websocket layer to SSE</tspan></text>
45
- <text class="mono" x="120" y="328" font-size="20"><tspan class="muted">assistant &#8250;</tspan> <tspan class="muted">on it &#8212; reading current transport&#8230;</tspan></text>
46
- </g>
47
- <line x1="120" y1="362" x2="1160" y2="362" stroke="#2A2620" stroke-dasharray="2 6"/>
48
-
49
- <text class="mono" x="120" y="408" font-size="22"><tspan class="mint" font-weight="700">&#10095;</tspan> <tspan class="accent" font-weight="700">/ask</tspan> <tspan class="ink">why did we switch from sockets to SSE?</tspan></text>
50
- <text class="mono" x="120" y="464" font-size="20"><tspan class="muted"> &#8250; resilience: SSE survives proxies that drop ws,</tspan></text>
51
- <text class="mono" x="120" y="494" font-size="20"><tspan class="muted"> and the team was tired of the ping-pong reconnect.</tspan></text>
52
- <text class="mono" x="120" y="540" font-size="20"><tspan class="dim"> &#8250;</tspan> <tspan class="mint" font-weight="700">EPHEMERAL &#183; NEVER WRITTEN</tspan></text>
53
-
54
- <text class="mono muted" x="120" y="618" font-size="14" letter-spacing="2.2">READ-ONLY BRANCH CLONE &#183; OWN AbortController &#183; tools: []</text>
55
-
56
- <rect x="64" y="708" width="1152" height="36" fill="#16120E"/>
57
- <line x1="64" y1="708" x2="1216" y2="708" stroke="#2A2620"/>
58
- <text class="mono" font-size="14" y="731" letter-spacing="1">
59
- <tspan x="92" class="accent" font-weight="700">&#9612; dm-ask</tspan>
60
- <tspan dx="14" class="dim">&#9474;</tspan>
61
- <tspan dx="14" class="mint">ephemeral</tspan>
62
- <tspan dx="14" class="dim">&#9474;</tspan>
63
- <tspan dx="14" class="muted">pi 0.9.4</tspan>
64
- <tspan dx="14" class="dim">&#9474;</tspan>
65
- <tspan dx="14" class="muted">npm:dm-ask</tspan>
66
- </text>
67
- </g>
68
-
69
- <rect x="64" y="56" width="1152" height="688" rx="12" fill="none" stroke="#3A352D" stroke-width="1"/>
70
- </svg>
Binary file
@@ -1,147 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 540 1200" role="img" aria-label="dm-ask — Side questions without polluting the main conversation" preserveAspectRatio="xMidYMid meet">
2
- <title>dm-ask — Side questions without polluting the main conversation</title>
3
- <defs>
4
- <style>
5
- .mono { font-family: 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', ui-monospace, Menlo, Consolas, monospace; }
6
- .ink { fill: #F4EFE6; }
7
- .paper { fill: #0E0D0B; }
8
- .muted { fill: #7A7468; }
9
- .dim { fill: #4A463F; }
10
- .accent { fill: #FF7A3D; }
11
- .mint { fill: #6FD0B8; }
12
- .rule { stroke: #2A2620; }
13
- .rule-bright { stroke: #FF7A3D; }
14
- </style>
15
- <filter id="grain" x="0" y="0" width="100%" height="100%">
16
- <feTurbulence type="fractalNoise" baseFrequency="1.4" numOctaves="2" seed="19"/>
17
- <feColorMatrix values="0 0 0 0 0.96 0 0 0 0 0.94 0 0 0 0 0.88 0 0 0 0.06 0"/>
18
- </filter>
19
- <pattern id="grid" width="32" height="32" patternUnits="userSpaceOnUse">
20
- <path d="M32 0 H0 V32" fill="none" stroke="#161411" stroke-width="0.5"/>
21
- </pattern>
22
- <pattern id="dots" width="6" height="6" patternUnits="userSpaceOnUse">
23
- <circle cx="3" cy="3" r="0.6" fill="#231F1A"/>
24
- </pattern>
25
- <radialGradient id="vignette" cx="50%" cy="50%" r="70%">
26
- <stop offset="60%" stop-color="#0E0D0B" stop-opacity="0"/>
27
- <stop offset="100%" stop-color="#000" stop-opacity="0.55"/>
28
- </radialGradient>
29
- </defs>
30
-
31
- <rect width="540" height="1200" class="paper"/>
32
- <rect width="540" height="1200" fill="url(#grid)" opacity="0.55"/>
33
- <rect width="540" height="1200" fill="url(#dots)" opacity="0.4"/>
34
- <rect width="540" height="1200" filter="url(#grain)" opacity="0.5"/>
35
- <rect width="540" height="1200" fill="url(#vignette)"/>
36
-
37
- <g class="rule-bright" stroke-width="1" fill="none" opacity="0.95">
38
- <path d="M24 56 L40 56 M40 40 L40 56"/>
39
- <path d="M516 56 L500 56 M500 40 L500 56"/>
40
- <path d="M24 1144 L40 1144 M40 1160 L40 1144"/>
41
- <path d="M516 1144 L500 1144 M500 1160 L500 1144"/>
42
- </g>
43
-
44
- <line x1="40" y1="56" x2="500" y2="56" class="rule" stroke-width="1"/>
45
- <text class="mono muted" x="40" y="44" font-size="9" letter-spacing="2">@JUICESHARP &#183; PI EXTENSION &#183; NO. 005 &#8211; ASK</text>
46
- <text class="mono muted" x="500" y="44" font-size="9" letter-spacing="2" text-anchor="end">MIT &#183; v0.9 &#183; 540 &#215; 1200</text>
47
-
48
- <text class="mono ink" x="40" y="124" font-size="56" font-weight="700" letter-spacing="-2">rpiv<tspan class="accent">-</tspan>ask</text>
49
- <line x1="40" y1="138" x2="100" y2="138" class="rule-bright" stroke-width="2"/>
50
- <text class="mono muted" x="40" y="160" font-size="12" letter-spacing="0.3">A side question that doesn&#8217;t pollute the chat.</text>
51
- <text class="mono dim" x="40" y="178" font-size="9" letter-spacing="1.4">READ-ONLY CLONE &#183; EPHEMERAL &#183; NO DISK</text>
52
-
53
- <text class="mono muted" x="40" y="222" font-size="9" letter-spacing="2">FIG. 01 &#8212; NO-POLLUTION SIDECHANNEL</text>
54
- <text class="mono muted" x="500" y="222" font-size="9" letter-spacing="2" text-anchor="end">/ask &#8226; in memory only</text>
55
- <line x1="40" y1="232" x2="500" y2="232" class="rule" stroke-width="0.75"/>
56
-
57
- <!-- HERO: stacked — main transcript above (dim), ask overlay below (highlighted) -->
58
-
59
- <!-- TOP: main transcript (dim) -->
60
- <text class="mono dim" x="40" y="270" font-size="9" letter-spacing="1.8">MAIN TRANSCRIPT &#8226; UNTOUCHED</text>
61
- <g opacity="0.6" transform="translate(40 282)">
62
- <rect x="0" y="0" width="460" height="14" fill="#15120E" stroke="#221E18" stroke-width="0.5"/>
63
- <rect x="0" y="18" width="380" height="14" fill="#15120E" stroke="#221E18" stroke-width="0.5"/>
64
- <rect x="0" y="36" width="440" height="14" fill="#15120E" stroke="#221E18" stroke-width="0.5"/>
65
- <rect x="0" y="54" width="320" height="14" fill="#15120E" stroke="#221E18" stroke-width="0.5"/>
66
- <rect x="0" y="72" width="420" height="14" fill="#15120E" stroke="#221E18" stroke-width="0.5"/>
67
- <rect x="0" y="90" width="360" height="14" fill="#15120E" stroke="#221E18" stroke-width="0.5"/>
68
- <rect x="0" y="108" width="400" height="14" fill="#15120E" stroke="#221E18" stroke-width="0.5"/>
69
- <text class="mono muted" x="6" y="11" font-size="9.5">user &#8250; let&#8217;s migrate websockets to SSE</text>
70
- <text class="mono muted" x="6" y="29" font-size="9.5">assistant &#8250; on it &#8212; reading transport</text>
71
- <text class="mono muted" x="6" y="47" font-size="9.5">tool &#8250; read src/transport/socket.ts</text>
72
- <text class="mono muted" x="6" y="65" font-size="9.5">assistant &#8250; ready to draft plan</text>
73
- <text class="mono muted" x="6" y="83" font-size="9.5">user &#8250; quick question first&#8230;</text>
74
- <text class="mono muted" x="6" y="101" font-size="9.5">assistant &#8250; sure, what&#8217;s up?</text>
75
- <text class="mono muted" x="6" y="119" font-size="9.5">user &#8250; using /ask to keep this clean</text>
76
- </g>
77
-
78
- <!-- read-only clone arrow on right -->
79
- <g transform="translate(460 320)">
80
- <line x1="0" y1="0" x2="0" y2="80" class="rule-bright" stroke-width="1.4" stroke-dasharray="3 3"/>
81
- <path d="M-7 70 L0 84 L7 70" fill="none" stroke="#FF7A3D" stroke-width="1.4" stroke-linejoin="round" stroke-linecap="round"/>
82
- <text class="mono accent" x="-12" y="-2" font-size="9" letter-spacing="1.4" text-anchor="end" font-weight="700">READ-ONLY</text>
83
- <text class="mono muted" x="-12" y="14" font-size="9" letter-spacing="1.2" text-anchor="end">CLONE</text>
84
- <text class="mono muted" x="-12" y="28" font-size="9" letter-spacing="1.2" text-anchor="end">tools: []</text>
85
- </g>
86
-
87
- <!-- horizontal seam (editor row) -->
88
- <line x1="40" y1="436" x2="500" y2="436" class="rule" stroke-width="0.5" stroke-dasharray="1 4"/>
89
- <text class="mono dim" x="40" y="430" font-size="9" letter-spacing="1.8">EDITOR ROW</text>
90
- <text class="mono dim" x="500" y="430" font-size="9" letter-spacing="1.8" text-anchor="end">&#8595; OVERLAY ANCHORED HERE</text>
91
-
92
- <!-- BOTTOM: ask overlay (hero panel) -->
93
- <g transform="translate(40 450)">
94
- <rect x="0" y="0" width="460" height="540" fill="#13110D" stroke="#FF7A3D" stroke-width="1"/>
95
-
96
- <!-- Banner -->
97
- <rect x="0" y="0" width="460" height="28" fill="#1B0F08"/>
98
- <text class="mono accent" x="14" y="19" font-size="12" letter-spacing="1.4" font-weight="700">/ask</text>
99
- <text class="mono ink" x="60" y="19" font-size="11">why did we switch from sockets to SSE</text>
100
- <text class="mono ink" x="60" y="33" font-size="11" opacity="0">.</text>
101
- <text class="mono muted" x="446" y="19" font-size="9" letter-spacing="1.2" text-anchor="end">Esc</text>
102
-
103
- <!-- Echo line of question continuation -->
104
- <text class="mono muted" x="14" y="56" font-size="11">last week?</text>
105
-
106
- <!-- Thinking indicator (faint) -->
107
- <text class="mono dim" x="14" y="80" font-size="11">&#8230;</text>
108
-
109
- <!-- Answer body -->
110
- <text class="mono muted" x="14" y="110" font-size="11">&#8250; resilience: SSE survives proxies that</text>
111
- <text class="mono muted" x="14" y="126" font-size="11"> buffer or drop ws connections.</text>
112
- <text class="mono muted" x="14" y="146" font-size="11">&#8250; the team was tired of the ping-pong</text>
113
- <text class="mono muted" x="14" y="162" font-size="11"> reconnect dance under restrictive</text>
114
- <text class="mono muted" x="14" y="178" font-size="11"> corporate networks.</text>
115
- <text class="mono muted" x="14" y="198" font-size="11">&#8250; server-push only, so back-channel</text>
116
- <text class="mono muted" x="14" y="214" font-size="11"> moved to plain fetch &#8212; trades</text>
117
- <text class="mono muted" x="14" y="230" font-size="11"> bidirectionality for stability.</text>
118
- <text class="mono muted" x="14" y="250" font-size="11">&#8250; see commit b3e21f and the post-</text>
119
- <text class="mono muted" x="14" y="266" font-size="11"> mortem in thoughts/research/.</text>
120
-
121
- <!-- Prior history strip -->
122
- <line x1="14" y1="296" x2="446" y2="296" class="rule" stroke-width="0.5" stroke-dasharray="1 3"/>
123
- <text class="mono dim" x="14" y="312" font-size="9" letter-spacing="1.6">PRIOR /ask IN THIS SESSION</text>
124
- <text class="mono muted" x="14" y="332" font-size="10.5">&#9786; what was the SSE timeout default?</text>
125
- <text class="mono muted" x="14" y="348" font-size="10.5">&#8250; 60s; configurable via X-Heartbeat&#8230;</text>
126
- <text class="mono muted" x="14" y="368" font-size="10.5">&#9786; can curl tail it for debugging?</text>
127
- <text class="mono muted" x="14" y="384" font-size="10.5">&#8250; yes &#8212; curl -N URL streams events&#8230;</text>
128
-
129
- <!-- Footer of overlay -->
130
- <line x1="14" y1="488" x2="446" y2="488" class="rule" stroke-width="0.5" stroke-dasharray="1 3"/>
131
- <text class="mono dim" x="14" y="510" font-size="9" letter-spacing="1.4">&#8593;&#8595; scroll &#183; x clear &#183; Esc close</text>
132
- <text class="mono mint" x="446" y="510" font-size="9" letter-spacing="1.4" text-anchor="end" font-weight="700">EPHEMERAL</text>
133
- <text class="mono dim" x="446" y="524" font-size="9" letter-spacing="1.4" text-anchor="end">never written to disk</text>
134
- </g>
135
-
136
- <text class="mono muted" x="40" y="1024" font-size="9" letter-spacing="1.4">PROCESS-SCOPED via globalThis[Symbol.for("dm-ask")]</text>
137
- <text class="mono muted" x="40" y="1040" font-size="9" letter-spacing="1.4">SNAPSHOT INVALIDATED on session_compact / session_tree</text>
138
-
139
- <line x1="40" y1="1100" x2="500" y2="1100" class="rule" stroke-width="0.5" stroke-dasharray="1 3"/>
140
- <text class="mono muted" x="40" y="1118" font-size="9" letter-spacing="1.8">ASK &#8594; ANSWER &#8594; DISCARD</text>
141
-
142
- <line x1="40" y1="1144" x2="500" y2="1144" class="rule" stroke-width="1"/>
143
- <text class="mono muted" x="40" y="1168" font-size="9.5" letter-spacing="1.8">
144
- <tspan class="accent">&#9654;</tspan> DM install npm:dm-ask
145
- </text>
146
- <text class="mono muted" x="500" y="1168" font-size="9" letter-spacing="2" text-anchor="end">SIDECHANNEL</text>
147
- </svg>
@@ -1,17 +0,0 @@
1
- /**
2
- * dm-ask — DM extension entry point.
3
- *
4
- * Registers /ask command + 2 lifecycle hooks (message_end snapshot,
5
- * session_compact/tree invalidation). No tool, no model picker, no disk
6
- * persistence. History lives in process-scoped globalThis state — survives
7
- * /new, /fork, /reload, /resume; lost on DM process exit.
8
- */
9
-
10
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
- import { registerAskCommand, registerInvalidationHooks, registerMessageEndSnapshot } from "./ask.js";
12
-
13
- export default function (pi: ExtensionAPI): void {
14
- registerAskCommand(pi);
15
- registerMessageEndSnapshot(pi);
16
- registerInvalidationHooks(pi);
17
- }
@@ -1,44 +0,0 @@
1
- {
2
- "name": "dm-ask",
3
- "version": "1.5.0",
4
- "description": "DM extension that answers side questions while the main agent keeps running",
5
- "type": "module",
6
- "files": [
7
- "*.ts",
8
- "README.md",
9
- "LICENSE",
10
- "CHANGELOG.md",
11
- "prompts",
12
- "docs"
13
- ],
14
- "pi": {
15
- "extensions": [
16
- "./index.ts"
17
- ]
18
- },
19
- "keywords": [
20
- "duckmind",
21
- "dm",
22
- "ask",
23
- "side-question",
24
- "widget"
25
- ],
26
- "repository": {
27
- "type": "git",
28
- "url": "git+https://github.com/que-nguyen/dm.git",
29
- "directory": "extensions/dm-ask"
30
- },
31
- "bugs": {
32
- "url": "https://github.com/que-nguyen/dm/issues"
33
- },
34
- "homepage": "https://github.com/que-nguyen/dm#readme",
35
- "scripts": {
36
- "test": "vitest run"
37
- },
38
- "peerDependencies": {
39
- "@mariozechner/pi-ai": "*",
40
- "@mariozechner/pi-coding-agent": "*",
41
- "@mariozechner/pi-tui": "*"
42
- },
43
- "devDependencies": {}
44
- }
@@ -1,9 +0,0 @@
1
- You are answering a quick side question while the user's main DM session continues working.
2
-
3
- You are given the user's primary conversation as the message context — treat it as background. Do NOT try to "continue" the assistant's prior work or pick up a tool call mid-flight; the side question is its own self-contained ask.
4
-
5
- Answer directly and concisely. Prefer compact bullets or short paragraphs. Cite files, functions, and line numbers when grounding a claim in the context. If the context is insufficient to answer, say so briefly instead of guessing.
6
-
7
- You have NO tools available. You will NOT call tools, even if the prior assistant turns demonstrate tool use. Reply in plain text only.
8
-
9
- When a "Recent /ask questions across sessions" appendix is present below, treat it as a high-level pattern hint about what the user has been thinking about lately — useful only when the side question explicitly asks about patterns, trends, or recent topics.