Skip to content

Commit dfe728a

Browse files
committed
feat(acp): add inline file chips, slash commands, and session-aware prompt restore
1 parent 341deb5 commit dfe728a

File tree

6 files changed

+1474
-79
lines changed

6 files changed

+1474
-79
lines changed

src/lib/acp/history.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export interface ACPHistoryTurnStop {
1313
timestamp: number;
1414
}
1515

16+
export interface ACPHistoryUserMessageSnapshot {
17+
index: number;
18+
content: Record<string, unknown>[];
19+
}
20+
1621
export interface ACPHistoryEntry {
1722
sessionId: string;
1823
url: string;
@@ -21,6 +26,7 @@ export interface ACPHistoryEntry {
2126
title: string;
2227
preview: string;
2328
turnStops: ACPHistoryTurnStop[];
29+
userMessageSnapshots: ACPHistoryUserMessageSnapshot[];
2430
createdAt: string;
2531
updatedAt: string;
2632
}
@@ -58,7 +64,23 @@ function normalizeEntry(entry: Partial<ACPHistoryEntry> = {}): ACPHistoryEntry {
5864
})
5965
.filter((item): item is ACPHistoryTurnStop => Boolean(item))
6066
: [];
61-
67+
const userMessageSnapshots = Array.isArray(entry.userMessageSnapshots)
68+
? entry.userMessageSnapshots
69+
.map((item) => {
70+
const index = Number.isInteger(item?.index) ? Number(item.index) : -1;
71+
const content = Array.isArray(item?.content)
72+
? item.content.filter((block) => {
73+
return block && typeof block === "object";
74+
})
75+
: [];
76+
if (index < 0 || !content.length) return null;
77+
return {
78+
index,
79+
content,
80+
};
81+
})
82+
.filter((item): item is ACPHistoryUserMessageSnapshot => Boolean(item))
83+
: [];
6284
return {
6385
sessionId:
6486
typeof entry.sessionId === "string" ? entry.sessionId.trim() : "",
@@ -69,6 +91,7 @@ function normalizeEntry(entry: Partial<ACPHistoryEntry> = {}): ACPHistoryEntry {
6991
title: typeof entry.title === "string" ? entry.title.trim() : "",
7092
preview: typeof entry.preview === "string" ? entry.preview.trim() : "",
7193
turnStops,
94+
userMessageSnapshots,
7295
createdAt,
7396
updatedAt,
7497
};
@@ -136,6 +159,10 @@ const acpHistory = {
136159
normalized.turnStops.length > 0
137160
? normalized.turnStops
138161
: current.turnStops,
162+
userMessageSnapshots:
163+
normalized.userMessageSnapshots.length > 0
164+
? normalized.userMessageSnapshots
165+
: current.userMessageSnapshots,
139166
createdAt: current.createdAt,
140167
updatedAt: normalized.updatedAt || new Date().toISOString(),
141168
};

src/lib/acp/models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
Plan as ACPPlan,
33
StopReason as ACPStopReason,
44
ToolCall as ACPToolCall,
5+
AvailableCommand,
56
ContentBlock,
67
PlanEntry,
78
} from "@agentclientprotocol/sdk";
@@ -67,6 +68,7 @@ export interface Plan extends ACPPlan {
6768
}
6869

6970
export type { PlanEntry };
71+
export type { AvailableCommand };
7072

7173
export interface ChatMessage {
7274
id: string;

src/lib/acp/session.ts

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
AvailableCommand,
23
ContentBlock,
34
RequestPermissionRequest as PermissionRequest,
45
SessionUpdate,
@@ -23,6 +24,7 @@ type SessionEventType =
2324
| "timeline_update"
2425
| "tool_call"
2526
| "tool_call_update"
27+
| "available_commands"
2628
| "plan"
2729
| "permission_request"
2830
| "session_end";
@@ -36,6 +38,7 @@ export class ACPSession {
3638
readonly timeline: TimelineEntry[] = [];
3739
readonly toolCalls: Map<string, ToolCall> = new Map();
3840
readonly turnStops: TurnStop[] = [];
41+
availableCommands: AvailableCommand[] = [];
3942
plan: Plan | null = null;
4043
title: string | null = null;
4144
updatedAt: string | null = null;
@@ -120,6 +123,9 @@ export class ACPSession {
120123
case "plan":
121124
this.handlePlan(update.entries);
122125
break;
126+
case "available_commands_update":
127+
this.handleAvailableCommandsUpdate(update.availableCommands);
128+
break;
123129
case "session_info_update":
124130
this.handleSessionInfoUpdate(update);
125131
break;
@@ -202,30 +208,13 @@ export class ACPSession {
202208
}
203209

204210
if (content.type === "text") {
205-
if (role === "agent") {
206-
this.agentTextBuffer += content.text;
207-
} else if (role === "thought") {
208-
this.thoughtTextBuffer += content.text;
209-
} else {
210-
this.userTextBuffer += content.text;
211-
}
212-
const existingText = message.content.find((c) => c.type === "text");
213-
if (existingText && existingText.type === "text") {
214-
existingText.text =
215-
role === "agent"
216-
? this.agentTextBuffer
217-
: role === "thought"
218-
? this.thoughtTextBuffer
219-
: this.userTextBuffer;
211+
const lastBlock = message.content[message.content.length - 1];
212+
if (lastBlock && lastBlock.type === "text") {
213+
lastBlock.text += content.text;
220214
} else {
221215
message.content.push({
222216
type: "text",
223-
text:
224-
role === "agent"
225-
? this.agentTextBuffer
226-
: role === "thought"
227-
? this.thoughtTextBuffer
228-
: this.userTextBuffer,
217+
text: content.text,
229218
});
230219
}
231220
} else {
@@ -325,6 +314,15 @@ export class ACPSession {
325314
this.emit("plan", this.plan);
326315
}
327316

317+
private handleAvailableCommandsUpdate(
318+
availableCommands: AvailableCommand[] | null | undefined,
319+
): void {
320+
this.availableCommands = Array.isArray(availableCommands)
321+
? [...availableCommands]
322+
: [];
323+
this.emit("available_commands", this.availableCommands);
324+
}
325+
328326
private handleSessionInfoUpdate(update: {
329327
title?: string | null;
330328
updatedAt?: string | null;
@@ -475,6 +473,7 @@ export class ACPSession {
475473
dispose(): void {
476474
this.listeners.clear();
477475
this.pendingPermissions.length = 0;
476+
this.availableCommands = [];
478477
this.currentAgentMessageId = null;
479478
this.currentThoughtMessageId = null;
480479
this.currentUserMessageId = null;

0 commit comments

Comments
 (0)