@ogulcancelik/pi-spar 0.1.0 → 0.1.1
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.
- package/README.md +19 -7
- package/assets/peek-demo.jpg +0 -0
- package/core.ts +25 -2
- package/index.ts +159 -94
- package/package.json +2 -1
- package/peek.ts +20 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ pi install npm:@ogulcancelik/pi-spar
|
|
|
13
13
|
Configure which models are available for sparring:
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
/
|
|
16
|
+
/spmodels
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
This shows all models from your pi configuration and lets you assign short aliases (e.g., `opus`, `gpt`).
|
|
@@ -29,7 +29,7 @@ The extension provides a `spar` tool the agent can use, plus commands for viewin
|
|
|
29
29
|
The agent uses this automatically when you ask it to consult another model:
|
|
30
30
|
|
|
31
31
|
```
|
|
32
|
-
"spar with
|
|
32
|
+
"spar with gpt about whether this architecture makes sense"
|
|
33
33
|
"ask opus to review the error handling in src/auth.ts"
|
|
34
34
|
```
|
|
35
35
|
|
|
@@ -39,13 +39,15 @@ Sessions persist — follow up, push back, disagree. The peer can read files, gr
|
|
|
39
39
|
|
|
40
40
|
| Command | Description |
|
|
41
41
|
|---------|-------------|
|
|
42
|
-
| `/
|
|
43
|
-
| `/
|
|
44
|
-
| `/
|
|
42
|
+
| `/spmodels` | Configure available sparring models |
|
|
43
|
+
| `/spar [session]` | Watch a spar session in a floating overlay |
|
|
44
|
+
| `/spview` | Browse all sessions — view, peek, or delete |
|
|
45
45
|
|
|
46
|
-
### Peek
|
|
46
|
+
### Peek overlay
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
`/spar` opens a floating overlay that renders the spar conversation using the same components as pi's main TUI — same message styling, same syntax-highlighted tool output, same everything. It's pi inside pi.
|
|
49
|
+
|
|
50
|
+

|
|
49
51
|
|
|
50
52
|
- **j/k** or **↑/↓** — scroll
|
|
51
53
|
- **g/G** — jump to top/bottom
|
|
@@ -53,6 +55,16 @@ The peek overlay renders the spar conversation using the same components as pi's
|
|
|
53
55
|
|
|
54
56
|
Live sessions auto-scroll as the peer model responds.
|
|
55
57
|
|
|
58
|
+
### Session browser
|
|
59
|
+
|
|
60
|
+
`/spview` opens an inline session browser:
|
|
61
|
+
|
|
62
|
+
- **j/k** or **↑/↓** — navigate
|
|
63
|
+
- **enter** — open peek overlay for selected session
|
|
64
|
+
- **d** — delete selected session
|
|
65
|
+
- **D** — delete all non-active sessions
|
|
66
|
+
- **q** or **Esc** — close
|
|
67
|
+
|
|
56
68
|
## License
|
|
57
69
|
|
|
58
70
|
MIT
|
|
Binary file
|
package/core.ts
CHANGED
|
@@ -222,7 +222,8 @@ class EventBroadcaster {
|
|
|
222
222
|
// Track state for sync on connect
|
|
223
223
|
private currentStatus: "thinking" | "streaming" | "tool" | "done" = "thinking";
|
|
224
224
|
private currentToolName?: string;
|
|
225
|
-
private currentPartialMessage: any = null;
|
|
225
|
+
private currentPartialMessage: any = null;
|
|
226
|
+
private currentUserMessage: any = null;
|
|
226
227
|
|
|
227
228
|
constructor(sessionId: string) {
|
|
228
229
|
this.socketPath = getSocketPath(sessionId);
|
|
@@ -244,6 +245,7 @@ class EventBroadcaster {
|
|
|
244
245
|
status: this.currentStatus,
|
|
245
246
|
toolName: this.currentToolName,
|
|
246
247
|
partialMessage: this.currentPartialMessage,
|
|
248
|
+
userMessage: this.currentUserMessage,
|
|
247
249
|
};
|
|
248
250
|
try { conn.write(JSON.stringify(syncEvent) + "\n"); } catch {}
|
|
249
251
|
|
|
@@ -262,7 +264,9 @@ class EventBroadcaster {
|
|
|
262
264
|
|
|
263
265
|
broadcast(event: any): void {
|
|
264
266
|
// Track state for sync
|
|
265
|
-
if (event.type === "message_start" && event.message?.role === "
|
|
267
|
+
if (event.type === "message_start" && event.message?.role === "user") {
|
|
268
|
+
this.currentUserMessage = event.message;
|
|
269
|
+
} else if (event.type === "message_start" && event.message?.role === "assistant") {
|
|
266
270
|
this.currentPartialMessage = event.message;
|
|
267
271
|
this.currentStatus = "thinking";
|
|
268
272
|
} else if (event.type === "message_update" && event.message?.role === "assistant") {
|
|
@@ -279,6 +283,7 @@ class EventBroadcaster {
|
|
|
279
283
|
this.currentToolName = event.toolName;
|
|
280
284
|
} else if (event.type === "message_end" || event.type === "agent_end") {
|
|
281
285
|
this.currentPartialMessage = null;
|
|
286
|
+
this.currentUserMessage = null;
|
|
282
287
|
this.currentStatus = "done";
|
|
283
288
|
this.currentToolName = undefined;
|
|
284
289
|
}
|
|
@@ -443,6 +448,24 @@ export function sessionExists(name: string): boolean {
|
|
|
443
448
|
return fs.existsSync(getSessionInfoPath(name));
|
|
444
449
|
}
|
|
445
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Delete a session and all its files (.jsonl, .info.json, .log)
|
|
453
|
+
*/
|
|
454
|
+
export function deleteSession(name: string): void {
|
|
455
|
+
validateSessionName(name);
|
|
456
|
+
const files = [
|
|
457
|
+
getSessionFilePath(name),
|
|
458
|
+
getSessionInfoPath(name),
|
|
459
|
+
getSessionLogPath(name),
|
|
460
|
+
];
|
|
461
|
+
for (const f of files) {
|
|
462
|
+
try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch {}
|
|
463
|
+
}
|
|
464
|
+
// Clean up socket if still around
|
|
465
|
+
const socketPath = getSocketPath(name);
|
|
466
|
+
try { if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath); } catch {}
|
|
467
|
+
}
|
|
468
|
+
|
|
446
469
|
/**
|
|
447
470
|
* Get session info
|
|
448
471
|
*/
|
package/index.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Spar Extension - Agent-to-agent sparring
|
|
3
3
|
*
|
|
4
4
|
* Provides a `spar` tool for back-and-forth dialogue with peer AI models,
|
|
5
|
-
* plus /
|
|
5
|
+
* plus /spar and /spview commands for viewing spar sessions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
10
10
|
import { Type } from "@sinclair/typebox";
|
|
11
|
-
import { Text, Container, Spacer, SelectList, Input, matchesKey, type SelectItem, type SelectListTheme } from "@mariozechner/pi-tui";
|
|
11
|
+
import { Text, Container, Spacer, SelectList, Input, matchesKey, truncateToWidth, type SelectItem, type SelectListTheme } from "@mariozechner/pi-tui";
|
|
12
12
|
import {
|
|
13
13
|
sendMessage,
|
|
14
14
|
listSessions,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getConfiguredModelsDescription,
|
|
18
18
|
loadSparConfig,
|
|
19
19
|
saveSparConfig,
|
|
20
|
+
deleteSession,
|
|
20
21
|
type SparModelConfig,
|
|
21
22
|
DEFAULT_TIMEOUT,
|
|
22
23
|
} from "./core.js";
|
|
@@ -48,7 +49,7 @@ function suggestAlias(provider: string, modelId: string): string {
|
|
|
48
49
|
|
|
49
50
|
export default function (pi: ExtensionAPI) {
|
|
50
51
|
// ==========================================================================
|
|
51
|
-
// Tool Registration —
|
|
52
|
+
// Tool Registration — model aliases are configured via /spmodels
|
|
52
53
|
// ==========================================================================
|
|
53
54
|
|
|
54
55
|
pi.registerTool({
|
|
@@ -117,7 +118,7 @@ spar({
|
|
|
117
118
|
description: "Message to send (required for send)",
|
|
118
119
|
})),
|
|
119
120
|
model: Type.Optional(Type.String({
|
|
120
|
-
description: "Model alias (from /
|
|
121
|
+
description: "Model alias (from /spmodels) or provider:model. Required for first message in a session.",
|
|
121
122
|
})),
|
|
122
123
|
thinking: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high"] as const, {
|
|
123
124
|
description: "Thinking level (default: high)",
|
|
@@ -437,14 +438,14 @@ spar({
|
|
|
437
438
|
});
|
|
438
439
|
|
|
439
440
|
// ==========================================================================
|
|
440
|
-
// Command: /
|
|
441
|
+
// Command: /spmodels — configure available sparring models
|
|
441
442
|
// ==========================================================================
|
|
442
443
|
|
|
443
|
-
pi.registerCommand("
|
|
444
|
+
pi.registerCommand("spmodels", {
|
|
444
445
|
description: "Configure models available for sparring",
|
|
445
446
|
handler: async (_args, ctx) => {
|
|
446
447
|
if (!ctx.hasUI) {
|
|
447
|
-
ctx.ui.notify("Interactive mode required for /
|
|
448
|
+
ctx.ui.notify("Interactive mode required for /spmodels", "warning");
|
|
448
449
|
return;
|
|
449
450
|
}
|
|
450
451
|
|
|
@@ -603,11 +604,11 @@ spar({
|
|
|
603
604
|
});
|
|
604
605
|
|
|
605
606
|
// ==========================================================================
|
|
606
|
-
// Commands: /
|
|
607
|
+
// Commands: /spar and /spview
|
|
607
608
|
// ==========================================================================
|
|
608
609
|
|
|
609
|
-
pi.registerCommand("
|
|
610
|
-
description: "Peek at a spar session. Usage: /
|
|
610
|
+
pi.registerCommand("spar", {
|
|
611
|
+
description: "Peek at a spar session. Usage: /spar [session-name]",
|
|
611
612
|
getArgumentCompletions: (prefix: string) => {
|
|
612
613
|
const sessions = listPeekableSessions();
|
|
613
614
|
const items = sessions.map((s) => ({
|
|
@@ -632,7 +633,7 @@ spar({
|
|
|
632
633
|
if (!sessionId) {
|
|
633
634
|
const available = listPeekableSessions();
|
|
634
635
|
if (available.length > 0) {
|
|
635
|
-
ctx.ui.notify(`No recent spar. Try: /
|
|
636
|
+
ctx.ui.notify(`No recent spar. Try: /spar ${available[0].name}`, "info");
|
|
636
637
|
} else {
|
|
637
638
|
ctx.ui.notify("No spar sessions found", "info");
|
|
638
639
|
}
|
|
@@ -660,100 +661,164 @@ spar({
|
|
|
660
661
|
},
|
|
661
662
|
});
|
|
662
663
|
|
|
663
|
-
pi.registerCommand("
|
|
664
|
-
description: "
|
|
664
|
+
pi.registerCommand("spview", {
|
|
665
|
+
description: "Browse spar sessions — view, peek, or delete",
|
|
665
666
|
handler: async (_args, ctx) => {
|
|
666
|
-
const
|
|
667
|
+
const result = await ctx.ui.custom<{ action: "peek"; name: string } | undefined>(
|
|
668
|
+
(tui, theme, _kb, done) => {
|
|
669
|
+
let selectedIndex = 0;
|
|
670
|
+
let cachedLines: string[] | undefined;
|
|
667
671
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
+
const fg = theme.fg.bind(theme);
|
|
673
|
+
|
|
674
|
+
function refresh() {
|
|
675
|
+
cachedLines = undefined;
|
|
676
|
+
tui.requestRender();
|
|
677
|
+
}
|
|
672
678
|
|
|
673
|
-
// Use custom component with SelectList for proper filtering/pagination
|
|
674
|
-
const selected = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
675
|
-
const items: SelectItem[] = sessions.map((s) => {
|
|
676
|
-
const status = s.active ? "●" : "○";
|
|
677
|
-
const model = s.model ? `[${s.model}]` : "";
|
|
678
|
-
const age = s.lastActivity ? formatAge(s.lastActivity) : "";
|
|
679
|
-
const msgs = s.messageCount > 0 ? `${s.messageCount}msg` : "";
|
|
680
|
-
// Format: "● session-name [gpt5] 3msg 2h"
|
|
681
|
-
const desc = [model, msgs, age].filter(Boolean).join(" ");
|
|
682
679
|
return {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
description: desc,
|
|
686
|
-
};
|
|
687
|
-
});
|
|
680
|
+
render(width: number): string[] {
|
|
681
|
+
if (cachedLines) return cachedLines;
|
|
688
682
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
selectedText: (t: string) => theme.bg("selectedBg", t),
|
|
692
|
-
description: (t: string) => theme.fg("muted", t),
|
|
693
|
-
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
694
|
-
noMatch: (t: string) => theme.fg("warning", t),
|
|
695
|
-
});
|
|
683
|
+
const sessions = listPeekableSessions();
|
|
684
|
+
if (selectedIndex >= sessions.length) selectedIndex = Math.max(0, sessions.length - 1);
|
|
696
685
|
|
|
697
|
-
|
|
698
|
-
selectList.onCancel = () => done(null);
|
|
686
|
+
const lines: string[] = [];
|
|
699
687
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const updateFilterDisplay = () => {
|
|
705
|
-
if (filter) {
|
|
706
|
-
filterText.text = theme.fg("dim", "Filter: ") + theme.fg("accent", filter) + theme.fg("dim", "▏");
|
|
707
|
-
} else {
|
|
708
|
-
filterText.text = theme.fg("dim", "Type to filter...");
|
|
709
|
-
}
|
|
710
|
-
};
|
|
711
|
-
updateFilterDisplay();
|
|
712
|
-
|
|
713
|
-
const container = new Container();
|
|
714
|
-
container.addChild(new Text(theme.fg("accent", "Spar Sessions") + theme.fg("dim", " (↑↓ navigate, enter select, esc cancel)"), 0, 1));
|
|
715
|
-
container.addChild(filterText);
|
|
716
|
-
container.addChild(selectList);
|
|
717
|
-
|
|
718
|
-
(container as any).handleInput = (data: string) => {
|
|
719
|
-
if (matchesKey(data, "escape")) {
|
|
720
|
-
done(null);
|
|
721
|
-
} else if (matchesKey(data, "return")) {
|
|
722
|
-
selectList.handleInput(data);
|
|
723
|
-
} else if (matchesKey(data, "up") || matchesKey(data, "down")) {
|
|
724
|
-
selectList.handleInput(data);
|
|
725
|
-
tui.requestRender();
|
|
726
|
-
} else if (matchesKey(data, "backspace")) {
|
|
727
|
-
filter = filter.slice(0, -1);
|
|
728
|
-
selectList.setFilter(filter);
|
|
729
|
-
updateFilterDisplay();
|
|
730
|
-
tui.requestRender();
|
|
731
|
-
} else if (data.length === 1 && data >= " ") {
|
|
732
|
-
filter += data;
|
|
733
|
-
selectList.setFilter(filter);
|
|
734
|
-
updateFilterDisplay();
|
|
735
|
-
tui.requestRender();
|
|
736
|
-
}
|
|
737
|
-
};
|
|
688
|
+
lines.push(fg("accent", "─".repeat(width)));
|
|
689
|
+
lines.push(fg("accent", theme.bold(" Spar Sessions")) + fg("dim", ` (${sessions.length})`));
|
|
690
|
+
lines.push("");
|
|
738
691
|
|
|
739
|
-
|
|
740
|
-
|
|
692
|
+
if (sessions.length === 0) {
|
|
693
|
+
lines.push(fg("dim", " No spar sessions found"));
|
|
694
|
+
} else {
|
|
695
|
+
// Table header
|
|
696
|
+
const header = [
|
|
697
|
+
"name".padEnd(30),
|
|
698
|
+
"model".padEnd(8),
|
|
699
|
+
"msgs".padStart(5),
|
|
700
|
+
"age".padStart(6),
|
|
701
|
+
].join(" ");
|
|
702
|
+
lines.push(fg("dim", ` ${header}`));
|
|
703
|
+
|
|
704
|
+
// Paginated viewport — show PAGE_SIZE rows around selection
|
|
705
|
+
const PAGE_SIZE = 15;
|
|
706
|
+
let viewStart = 0;
|
|
707
|
+
if (sessions.length > PAGE_SIZE) {
|
|
708
|
+
// Keep selection centered in viewport
|
|
709
|
+
viewStart = Math.max(0, Math.min(
|
|
710
|
+
selectedIndex - Math.floor(PAGE_SIZE / 2),
|
|
711
|
+
sessions.length - PAGE_SIZE,
|
|
712
|
+
));
|
|
713
|
+
}
|
|
714
|
+
const viewEnd = Math.min(viewStart + PAGE_SIZE, sessions.length);
|
|
741
715
|
|
|
742
|
-
|
|
716
|
+
if (viewStart > 0) {
|
|
717
|
+
lines.push(fg("dim", ` ↑ ${viewStart} more`));
|
|
718
|
+
}
|
|
743
719
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
720
|
+
for (let i = viewStart; i < viewEnd; i++) {
|
|
721
|
+
const s = sessions[i];
|
|
722
|
+
const isSelected = i === selectedIndex;
|
|
723
|
+
const prefix = isSelected ? fg("accent", "→ ") : " ";
|
|
724
|
+
const icon = s.active ? fg("success", "●") : fg("dim", "○");
|
|
725
|
+
|
|
726
|
+
const row = [
|
|
727
|
+
s.name.slice(0, 30).padEnd(30),
|
|
728
|
+
(s.model || "—").padEnd(8),
|
|
729
|
+
String(s.messageCount || 0).padStart(5),
|
|
730
|
+
(s.lastActivity ? formatAge(s.lastActivity) : "—").padStart(6),
|
|
731
|
+
].join(" ");
|
|
732
|
+
|
|
733
|
+
const color = isSelected ? "accent" : s.active ? "text" : "muted";
|
|
734
|
+
lines.push(truncateToWidth(`${prefix}${icon} ${fg(color, row)}`, width));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (viewEnd < sessions.length) {
|
|
738
|
+
lines.push(fg("dim", ` ↓ ${sessions.length - viewEnd} more`));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
lines.push("");
|
|
743
|
+
|
|
744
|
+
// Contextual help
|
|
745
|
+
const helpParts = ["↑↓ navigate"];
|
|
746
|
+
if (sessions.length > 0) {
|
|
747
|
+
helpParts.push("enter peek");
|
|
748
|
+
helpParts.push("d delete");
|
|
749
|
+
helpParts.push("D delete all");
|
|
750
|
+
}
|
|
751
|
+
helpParts.push("esc close");
|
|
752
|
+
lines.push(fg("dim", ` ${helpParts.join(" · ")}`));
|
|
753
|
+
|
|
754
|
+
lines.push(fg("accent", "─".repeat(width)));
|
|
755
|
+
|
|
756
|
+
cachedLines = lines;
|
|
757
|
+
return lines;
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
handleInput(data: string): void {
|
|
761
|
+
const sessions = listPeekableSessions();
|
|
762
|
+
|
|
763
|
+
if (matchesKey(data, "escape") || data === "q") {
|
|
764
|
+
done(undefined);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
768
|
+
selectedIndex = selectedIndex <= 0 ? sessions.length - 1 : selectedIndex - 1;
|
|
769
|
+
refresh();
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
773
|
+
selectedIndex = selectedIndex >= sessions.length - 1 ? 0 : selectedIndex + 1;
|
|
774
|
+
refresh();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (matchesKey(data, "return")) {
|
|
778
|
+
if (sessions.length > 0 && sessions[selectedIndex]) {
|
|
779
|
+
done({ action: "peek", name: sessions[selectedIndex].name });
|
|
780
|
+
}
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (data === "d") {
|
|
784
|
+
if (sessions.length > 0 && sessions[selectedIndex]) {
|
|
785
|
+
deleteSession(sessions[selectedIndex].name);
|
|
786
|
+
refresh();
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (data === "D") {
|
|
791
|
+
for (const s of sessions) {
|
|
792
|
+
if (!s.active) deleteSession(s.name);
|
|
793
|
+
}
|
|
794
|
+
refresh();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
|
|
799
|
+
invalidate(): void {
|
|
800
|
+
cachedLines = undefined;
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
},
|
|
756
804
|
);
|
|
805
|
+
|
|
806
|
+
// Open peek if selected
|
|
807
|
+
if (result?.action === "peek") {
|
|
808
|
+
await ctx.ui.custom<void>(
|
|
809
|
+
(tui, theme, _kb, done) => new SparPeekOverlay(tui, theme, result.name, done),
|
|
810
|
+
{
|
|
811
|
+
overlay: true,
|
|
812
|
+
overlayOptions: {
|
|
813
|
+
anchor: "right-center",
|
|
814
|
+
width: "45%",
|
|
815
|
+
minWidth: 50,
|
|
816
|
+
maxHeight: 60,
|
|
817
|
+
margin: { right: 2, top: 2, bottom: 2 },
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
);
|
|
821
|
+
}
|
|
757
822
|
},
|
|
758
823
|
});
|
|
759
824
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogulcancelik/pi-spar",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Agent-to-agent sparring for pi. Back-and-forth conversations with peer AI models for debugging, design review, and challenging your thinking.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"index.ts",
|
|
36
36
|
"core.ts",
|
|
37
37
|
"peek.ts",
|
|
38
|
+
"assets/peek-demo.jpg",
|
|
38
39
|
"README.md",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
]
|
package/peek.ts
CHANGED
|
@@ -355,6 +355,16 @@ export class SparPeekOverlay {
|
|
|
355
355
|
this.status = event.status || "thinking";
|
|
356
356
|
this.toolName = event.toolName;
|
|
357
357
|
|
|
358
|
+
// If there's a user message that hasn't been persisted to file yet, show it
|
|
359
|
+
if (event.userMessage) {
|
|
360
|
+
const text = this.getUserText(event.userMessage);
|
|
361
|
+
if (text) {
|
|
362
|
+
this.chatContainer.addChild(
|
|
363
|
+
new UserMessageComponent(text, getMarkdownTheme()),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
358
368
|
// If there's a partial message, use it directly (faithful reconstruction)
|
|
359
369
|
if (event.partialMessage) {
|
|
360
370
|
this.streamingMessage = event.partialMessage;
|
|
@@ -383,8 +393,16 @@ export class SparPeekOverlay {
|
|
|
383
393
|
}
|
|
384
394
|
}
|
|
385
395
|
} else if (event.type === "message_start") {
|
|
386
|
-
|
|
387
|
-
|
|
396
|
+
if (event.message?.role === "user") {
|
|
397
|
+
// User message — render immediately (same as interactive-mode)
|
|
398
|
+
const text = this.getUserText(event.message);
|
|
399
|
+
if (text) {
|
|
400
|
+
this.chatContainer.addChild(
|
|
401
|
+
new UserMessageComponent(text, getMarkdownTheme()),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
} else if (event.message?.role === "assistant") {
|
|
405
|
+
// New assistant message — create streaming component
|
|
388
406
|
this.cleanupStreaming();
|
|
389
407
|
this.streamingMessage = event.message;
|
|
390
408
|
this.streamingComponent = new AssistantMessageComponent(
|