@yuangc/paseo-plugin-workspace-overview 0.0.0-stage → 1.0.0

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 (68) hide show
  1. package/client/agent-preview/PreviewActions.tsx +56 -0
  2. package/client/agent-preview/useAutoCommit.ts +91 -0
  3. package/client/components/AgentCommandMenu.tsx +51 -0
  4. package/client/components/AgentConversationPreview.tsx +620 -0
  5. package/client/components/AgentConversationTimeline.tsx +177 -0
  6. package/client/components/AgentCreatorPanel.tsx +152 -0
  7. package/client/components/AgentQuestionCard.tsx +244 -0
  8. package/client/components/AgentRow.tsx +108 -0
  9. package/client/components/BadgeWithTooltip.tsx +119 -0
  10. package/client/components/BranchRow.tsx +121 -0
  11. package/client/components/ControlGlyph.tsx +18 -0
  12. package/client/components/HeatmapCalendar.tsx +165 -0
  13. package/client/components/ModelBrowserMenu.tsx +135 -0
  14. package/client/components/PaseoAssistantMessage.tsx +216 -0
  15. package/client/components/PaseoShellDetail.tsx +128 -0
  16. package/client/components/PaseoStreamBadge.tsx +94 -0
  17. package/client/components/PaseoTimelineItem.tsx +147 -0
  18. package/client/components/PaseoToolCallGroup.tsx +52 -0
  19. package/client/components/PaseoToolDetail.tsx +55 -0
  20. package/client/components/ProjectCard.tsx +196 -0
  21. package/client/components/ProjectIcon.tsx +49 -0
  22. package/client/components/ProviderBrandIcon.tsx +52 -0
  23. package/client/components/ResizeHandle.tsx +91 -0
  24. package/client/components/SharedComposerInput.tsx +383 -0
  25. package/client/components/Tooltip.tsx +49 -0
  26. package/client/components/WorkspaceAgentRow.tsx +93 -0
  27. package/client/components/WorkspaceBadge.tsx +39 -0
  28. package/client/components/WorkspaceCreateComposer.tsx +170 -0
  29. package/client/components/WorkspaceCreatorPanel.tsx +256 -0
  30. package/client/components/WorkspaceLaunchMenu.tsx +76 -0
  31. package/client/components/WorkspaceMenu.tsx +116 -0
  32. package/client/components/WorkspaceSelectControl.tsx +52 -0
  33. package/client/components/agent-conversation-types.ts +7 -0
  34. package/client/components/workspace-creator-styles.ts +322 -0
  35. package/client/hooks/useAgentActivityHeatmap.ts +178 -0
  36. package/client/hooks/useFilter.ts +107 -0
  37. package/client/hooks/useKeyboardInset.ts +37 -0
  38. package/client/hooks/useResizableWidth.ts +70 -0
  39. package/client/hooks/useWorkspaces.ts +128 -0
  40. package/client/overview/useProjectOptions.ts +73 -0
  41. package/client/web.ts +160 -0
  42. package/client/workspace-agents.tsx +71 -0
  43. package/client/workspace-creator/badge-styles.ts +13 -0
  44. package/client/workspace-creator/constants.ts +14 -0
  45. package/client/workspace-creator/control-styles.ts +12 -0
  46. package/client/workspace-creator/provider-selection.ts +51 -0
  47. package/client/workspace-creator/submit-workspace.ts +67 -0
  48. package/client/workspace-creator/types.ts +40 -0
  49. package/client/workspace-creator/ui-utils.ts +15 -0
  50. package/client/workspace-creator/useProviderCatalog.ts +89 -0
  51. package/client/workspace-overview.tsx +533 -0
  52. package/index.client.tsx +36 -0
  53. package/index.server.ts +33 -0
  54. package/package.json +28 -4
  55. package/paseo-plugin.json +4 -0
  56. package/server/agent-config-impl.ts +57 -0
  57. package/server/auto-commit-impl.ts +221 -0
  58. package/server/git-branches-impl.ts +81 -0
  59. package/server/project-icon-impl.ts +102 -0
  60. package/server/terminal-launch-impl.ts +56 -0
  61. package/shared/agent-activity.ts +16 -0
  62. package/shared/agent-config.ts +30 -0
  63. package/shared/auto-commit.ts +38 -0
  64. package/shared/git-branches.ts +18 -0
  65. package/shared/overview-types.ts +87 -0
  66. package/shared/project-icon.ts +13 -0
  67. package/shared/terminal-launch.ts +20 -0
  68. package/README.md +0 -3
@@ -0,0 +1,620 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Pressable, Text, View, type TextStyle, type ViewStyle } from "react-native";
3
+ import { useRpc, type PluginSurfaceProps } from "@getpaseo/plugin/client";
4
+ import { Icon } from "@getpaseo/plugin/client/react-native";
5
+ import type { ComposerSelection, PaseoClient, ProviderSnapshot } from "../workspace-creator/types";
6
+ import type { AgentEntry } from "../../shared/overview-types";
7
+ import type { AgentTimelineEntry } from "./agent-conversation-types";
8
+ import { AgentConversationTimeline } from "./AgentConversationTimeline";
9
+ import { ResizeHandle } from "./ResizeHandle";
10
+ import { createComposerStyles } from "./workspace-creator-styles";
11
+ import { SharedComposerInput } from "./SharedComposerInput";
12
+ import { agentCancelRpc, agentConfigSetRpc, agentRewindRpc } from "../../shared/agent-config";
13
+ import { buildSelection } from "../workspace-creator/provider-selection";
14
+ import { useAutoCommit } from "../agent-preview/useAutoCommit";
15
+ import { AutoCommitErrorToast, QuickActionButton } from "../agent-preview/PreviewActions";
16
+ import type { AgentPermissionRequest, AgentPermissionResponse, AgentUsage } from "@getpaseo/protocol/agent-types";
17
+ import { AgentCommandMenu, filterAgentCommands, type AgentSlashCommand } from "./AgentCommandMenu";
18
+
19
+ type AgentTimelineCursor = { epoch: string; seq: number };
20
+ type QueuedMessage = { id: string; text: string };
21
+
22
+ const queueStorageKey = (agentId: string) => `workspace-overview:agent-queue:${agentId}`;
23
+
24
+ function loadQueuedMessages(agentId: string): QueuedMessage[] {
25
+ if (typeof localStorage === "undefined") return [];
26
+ try {
27
+ const value = JSON.parse(localStorage.getItem(queueStorageKey(agentId)) ?? "[]");
28
+ return Array.isArray(value)
29
+ ? value.filter((item): item is QueuedMessage => typeof item?.id === "string" && typeof item?.text === "string")
30
+ : [];
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ function saveQueuedMessages(agentId: string, messages: QueuedMessage[]) {
37
+ if (typeof localStorage === "undefined") return;
38
+ try {
39
+ if (messages.length === 0) localStorage.removeItem(queueStorageKey(agentId));
40
+ else localStorage.setItem(queueStorageKey(agentId), JSON.stringify(messages));
41
+ } catch {
42
+ // Queueing still works in memory when browser storage is unavailable.
43
+ }
44
+ }
45
+
46
+ function getColors(theme: PluginSurfaceProps["theme"]) {
47
+ const fallback = {
48
+ surface0: "#1e1e1e",
49
+ foreground: "#ffffff",
50
+ foregroundMuted: "#888888",
51
+ accent: "#007acc",
52
+ accentForeground: "#ffffff",
53
+ statusDanger: "#ef4444",
54
+ statusSuccess: "#22c55e",
55
+ };
56
+ if (!theme || !theme.colors) {
57
+ return fallback;
58
+ }
59
+ return { ...fallback, ...theme.colors };
60
+ }
61
+
62
+ export function AgentConversationPreview({
63
+ agent,
64
+ workspaceDirectory,
65
+ paseo,
66
+ theme,
67
+ onClose,
68
+ onOpenFull,
69
+ initialPanelWidth = 480,
70
+ maxPanelWidth = 800,
71
+ compact = false,
72
+ }: {
73
+ agent: AgentEntry;
74
+ workspaceDirectory?: string;
75
+ paseo: PaseoClient;
76
+ theme: PluginSurfaceProps["theme"];
77
+ onClose: () => void;
78
+ onOpenFull: () => void;
79
+ initialPanelWidth?: number;
80
+ maxPanelWidth?: number;
81
+ compact?: boolean;
82
+ }) {
83
+ const handle = useMemo(() => paseo.agents.ref(agent.id), [agent.id, paseo]);
84
+ const [entries, setEntries] = useState<AgentTimelineEntry[]>([]);
85
+ const [prompt, setPrompt] = useState("");
86
+ const [loading, setLoading] = useState(true);
87
+ const [error, setError] = useState<string | null>(null);
88
+ const [sending, setSending] = useState(false);
89
+ const [configPending, setConfigPending] = useState(false);
90
+ const [cancelling, setCancelling] = useState(false);
91
+ const [composerSelection, setComposerSelection] = useState<ComposerSelection | null>(null);
92
+ const [providerSnapshot, setProviderSnapshot] = useState<ProviderSnapshot | null>(null);
93
+ const [openMenu, setOpenMenu] = useState<"model" | "mode" | "thinking" | null>(null);
94
+ const [liveStatus, setLiveStatus] = useState(agent.status);
95
+ const [pendingPermissions, setPendingPermissions] = useState<AgentPermissionRequest[]>([]);
96
+ const [respondingRequestId, setRespondingRequestId] = useState<string | null>(null);
97
+ const [startCursor, setStartCursor] = useState<AgentTimelineCursor | null>(null);
98
+ const [hasOlder, setHasOlder] = useState(false);
99
+ const [loadingOlder, setLoadingOlder] = useState(false);
100
+ const [scrollToEndVersion, setScrollToEndVersion] = useState(0);
101
+ const [commands, setCommands] = useState<AgentSlashCommand[]>([]);
102
+ const [commandsLoading, setCommandsLoading] = useState(false);
103
+ const [commandsError, setCommandsError] = useState<string | null>(null);
104
+ const [activeCommandIndex, setActiveCommandIndex] = useState(0);
105
+ const [lastUsage, setLastUsage] = useState<AgentUsage | null>(null);
106
+ const [activeTurnStartedAt, setActiveTurnStartedAt] = useState<string | null>(null);
107
+ const [lastAgentError, setLastAgentError] = useState<string | null>(null);
108
+ const [queuedState, setQueuedState] = useState<{ agentId: string; messages: QueuedMessage[] }>(() => ({
109
+ agentId: agent.id,
110
+ messages: loadQueuedMessages(agent.id),
111
+ }));
112
+ const queuedMessages = queuedState.agentId === agent.id ? queuedState.messages : [];
113
+ const updateQueuedMessages = useCallback((update: (current: QueuedMessage[]) => QueuedMessage[]) => {
114
+ setQueuedState((currentState) => {
115
+ const current = currentState.agentId === agent.id ? currentState.messages : loadQueuedMessages(agent.id);
116
+ const messages = update(current);
117
+ saveQueuedMessages(agent.id, messages);
118
+ return { agentId: agent.id, messages };
119
+ });
120
+ }, [agent.id]);
121
+ const [rewindingMessageId, setRewindingMessageId] = useState<string | null>(null);
122
+ const loadingRef = useRef(false);
123
+ const historyInitializedRef = useRef(false);
124
+ const latestSeqRef = useRef<number | undefined>(undefined);
125
+ const setAgentConfig = useRpc(agentConfigSetRpc);
126
+ const cancelAgent = useRpc(agentCancelRpc);
127
+ const rewindAgent = useRpc(agentRewindRpc);
128
+ const commitCwd = workspaceDirectory ?? agent.cwd;
129
+
130
+ useEffect(() => {
131
+ setQueuedState({ agentId: agent.id, messages: loadQueuedMessages(agent.id) });
132
+ }, [agent.id]);
133
+
134
+ const refresh = useCallback(async () => {
135
+ if (loadingRef.current) return;
136
+ loadingRef.current = true;
137
+ try {
138
+ const page = await handle.timeline.refetch({ direction: "tail", projection: "projected", limit: 200 });
139
+ const nextLatest = page.entries.at(-1)?.seqStart;
140
+ if (latestSeqRef.current !== nextLatest) {
141
+ latestSeqRef.current = nextLatest;
142
+ setScrollToEndVersion((version) => version + 1);
143
+ }
144
+ setEntries((current) => {
145
+ if (current.length === 0) return page.entries;
146
+ const merged = new Map(current.map((entry) => [entry.seqStart, entry]));
147
+ for (const entry of page.entries) merged.set(entry.seqStart, entry);
148
+ return Array.from(merged.values()).sort((left, right) => left.seqStart - right.seqStart);
149
+ });
150
+ if (!historyInitializedRef.current) {
151
+ historyInitializedRef.current = true;
152
+ setStartCursor(page.startCursor);
153
+ setHasOlder(page.hasOlder);
154
+ }
155
+ setError(page.error);
156
+ const agentSnapshot = (page as { agent?: { status?: string; currentModeId?: string | null } | null }).agent;
157
+ const usage = (page as { agent?: { lastUsage?: AgentUsage } | null }).agent?.lastUsage ?? handle.lastUsage;
158
+ setLastUsage(usage ?? null);
159
+ const activeTurn = (page as { agent?: { activeTurn?: { startedAt?: string | null } | null } | null }).agent?.activeTurn ?? handle.activeTurn;
160
+ setActiveTurnStartedAt(activeTurn?.startedAt ?? null);
161
+ const agentError = (page as { agent?: { lastError?: string | null } | null }).agent?.lastError ?? handle.lastError;
162
+ setLastAgentError(agentError ?? null);
163
+ const permissions = (page as { agent?: { pendingPermissions?: AgentPermissionRequest[] } | null }).agent?.pendingPermissions
164
+ ?? handle.pendingPermissions
165
+ ?? [];
166
+ setPendingPermissions(permissions);
167
+ if (agentSnapshot?.status) {
168
+ setLiveStatus(agentSnapshot.status as typeof liveStatus);
169
+ } else if (handle.status) {
170
+ setLiveStatus(handle.status);
171
+ }
172
+ } catch (refreshError) {
173
+ setError(refreshError instanceof Error ? refreshError.message : String(refreshError));
174
+ } finally {
175
+ loadingRef.current = false;
176
+ setLoading(false);
177
+ }
178
+ }, [handle]);
179
+
180
+ const loadOlder = useCallback(async () => {
181
+ if (!startCursor || !hasOlder || loadingOlder) return;
182
+ setLoadingOlder(true);
183
+ setError(null);
184
+ try {
185
+ const page = await handle.timeline.refetch({ direction: "before", cursor: startCursor, projection: "projected", limit: 200 });
186
+ setEntries((current) => {
187
+ const merged = new Map(current.map((entry) => [entry.seqStart, entry]));
188
+ for (const entry of page.entries) merged.set(entry.seqStart, entry);
189
+ return Array.from(merged.values()).sort((left, right) => left.seqStart - right.seqStart);
190
+ });
191
+ setStartCursor(page.startCursor);
192
+ setHasOlder(page.hasOlder);
193
+ if (page.error) setError(page.error);
194
+ } catch (loadError) {
195
+ setError(loadError instanceof Error ? loadError.message : String(loadError));
196
+ } finally {
197
+ setLoadingOlder(false);
198
+ }
199
+ }, [handle, hasOlder, loadingOlder, startCursor]);
200
+
201
+ const respondToPermission = useCallback(async (requestId: string, response: AgentPermissionResponse) => {
202
+ if (respondingRequestId) return;
203
+ setRespondingRequestId(requestId);
204
+ setError(null);
205
+ try {
206
+ await handle.respondToPermission({ requestId, response });
207
+ setPendingPermissions((current) => current.filter((request) => request.id !== requestId));
208
+ setTimeout(() => void refresh(), 250);
209
+ } catch (responseError) {
210
+ setError(responseError instanceof Error ? responseError.message : String(responseError));
211
+ } finally {
212
+ setRespondingRequestId(null);
213
+ }
214
+ }, [handle, refresh, respondingRequestId]);
215
+
216
+ const autoCommit = useAutoCommit(commitCwd, refresh);
217
+
218
+ // Track live agent status via handle subscription + polling
219
+ useEffect(() => {
220
+ setLiveStatus(agent.status);
221
+ const updateStatus = () => {
222
+ const current = handle.current();
223
+ if (current?.status) setLiveStatus(current.status);
224
+ };
225
+ updateStatus();
226
+ const unsubscribe = handle.subscribe((update: Record<string, unknown>) => {
227
+ const status = (update as { status?: string }).status;
228
+ if (status) setLiveStatus(status as typeof liveStatus);
229
+ });
230
+ const timer = setInterval(updateStatus, 2000);
231
+ return () => { unsubscribe(); clearInterval(timer); };
232
+ }, [agent.id, agent.status, handle]);
233
+
234
+ useEffect(() => {
235
+ setEntries([]);
236
+ setCommands([]);
237
+ setCommandsError(null);
238
+ setLastAgentError(null);
239
+ historyInitializedRef.current = false;
240
+ latestSeqRef.current = undefined;
241
+ setStartCursor(null);
242
+ setHasOlder(false);
243
+ setLoading(true);
244
+ void refresh();
245
+ const unsubscribe = handle.timeline.subscribe(() => void refresh());
246
+ const timer = setInterval(() => void refresh(), agent.status === "running" ? 1200 : 4000);
247
+ return () => { unsubscribe(); clearInterval(timer); };
248
+ }, [agent.id, agent.status, handle, refresh]);
249
+
250
+ useEffect(() => {
251
+ let cancelled = false;
252
+ const provider = agent.provider as Parameters<typeof paseo.providers.listModels>[0];
253
+ void Promise.all([
254
+ paseo.providers.listModels(provider, { cwd: workspaceDirectory ?? agent.cwd }),
255
+ paseo.providers.listModes(provider, { cwd: workspaceDirectory ?? agent.cwd }),
256
+ ]).then(([modelsResult, modesResult]) => {
257
+ if (cancelled) return;
258
+ const models = modelsResult.models ?? [];
259
+ const modes = modesResult.modes ?? [];
260
+ const entry = {
261
+ provider: agent.provider,
262
+ label: agent.provider,
263
+ status: "ready" as const,
264
+ enabled: true,
265
+ models,
266
+ modes,
267
+ };
268
+ const model = models.find((item) => item.id === agent.model) ?? models[0] ?? null;
269
+ const next = buildSelection(entry, model);
270
+ setComposerSelection({
271
+ ...next,
272
+ modeId: agent.currentModeId ?? next.modeId,
273
+ modeLabel: modes.find((item) => item.id === agent.currentModeId)?.label ?? agent.currentModeId ?? next.modeLabel,
274
+ thinkingOptionId: agent.thinkingOptionId ?? next.thinkingOptionId,
275
+ thinkingLabel: model?.thinkingOptions?.find((item) => item.id === agent.thinkingOptionId)?.label ?? agent.thinkingOptionId ?? next.thinkingLabel,
276
+ });
277
+ setProviderSnapshot({ entries: [entry] });
278
+ }).catch((loadError: unknown) => {
279
+ if (!cancelled) setError(loadError instanceof Error ? loadError.message : String(loadError));
280
+ });
281
+ return () => { cancelled = true; };
282
+ }, [agent.currentModeId, agent.cwd, agent.model, agent.provider, agent.thinkingOptionId, paseo, workspaceDirectory]);
283
+
284
+ const updateAgentConfig = useCallback(async (field: "model" | "mode" | "thinking", value: string) => {
285
+ if (configPending) return;
286
+ setConfigPending(true);
287
+ setError(null);
288
+ try {
289
+ await setAgentConfig({ agentId: agent.id, field, value });
290
+ setComposerSelection((current) => {
291
+ if (!current) return current;
292
+ if (field === "model") {
293
+ const model = providerSnapshot?.entries[0]?.models?.find((item) => item.id === value);
294
+ return { ...current, modelId: value, modelLabel: model?.label ?? value, thinkingOptionId: model?.defaultThinkingOptionId ?? null, thinkingLabel: model?.thinkingOptions?.find((item) => item.id === model.defaultThinkingOptionId)?.label ?? model?.defaultThinkingOptionId ?? null };
295
+ }
296
+ if (field === "mode") {
297
+ const mode = providerSnapshot?.entries[0]?.modes?.find((item) => item.id === value);
298
+ return { ...current, modeId: value, modeLabel: mode?.label ?? value };
299
+ }
300
+ const model = providerSnapshot?.entries[0]?.models?.find((item) => item.id === current.modelId);
301
+ const thinking = model?.thinkingOptions?.find((item) => item.id === value);
302
+ return { ...current, thinkingOptionId: value, thinkingLabel: thinking?.label ?? value };
303
+ });
304
+ setOpenMenu(null);
305
+ void refresh();
306
+ } catch (updateError) {
307
+ setError(updateError instanceof Error ? updateError.message : String(updateError));
308
+ } finally {
309
+ setConfigPending(false);
310
+ }
311
+ }, [agent.id, configPending, providerSnapshot, refresh, setAgentConfig]);
312
+
313
+ const submit = useCallback(async () => {
314
+ const text = prompt.trim();
315
+ if (!text || sending || pendingPermissions.length > 0) return;
316
+ if (liveStatus === "running") {
317
+ updateQueuedMessages((current) => [...current, { id: `${Date.now()}-${current.length}`, text }]);
318
+ setPrompt("");
319
+ return;
320
+ }
321
+ setSending(true);
322
+ setError(null);
323
+ try {
324
+ await handle.send(text);
325
+ setPrompt("");
326
+ setLiveStatus("running");
327
+ setTimeout(() => void refresh(), 250);
328
+ } catch (sendError) {
329
+ setError(sendError instanceof Error ? sendError.message : String(sendError));
330
+ } finally {
331
+ setSending(false);
332
+ }
333
+ }, [handle, liveStatus, pendingPermissions.length, prompt, refresh, sending, updateQueuedMessages]);
334
+
335
+ useEffect(() => {
336
+ if (liveStatus === "running" || sending || pendingPermissions.length > 0 || queuedMessages.length === 0) return;
337
+ const next = queuedMessages[0];
338
+ setSending(true);
339
+ setError(null);
340
+ void handle.send(next.text).then(() => {
341
+ updateQueuedMessages((current) => current.filter((item) => item.id !== next.id));
342
+ setLiveStatus("running");
343
+ setTimeout(() => void refresh(), 250);
344
+ }).catch((sendError: unknown) => {
345
+ setError(sendError instanceof Error ? sendError.message : String(sendError));
346
+ }).finally(() => setSending(false));
347
+ }, [handle, liveStatus, pendingPermissions.length, queuedMessages, refresh, sending, updateQueuedMessages]);
348
+
349
+ const slashMatch = prompt.match(/^\/([^\s/]*)$/);
350
+ const commandQuery = slashMatch?.[1] ?? null;
351
+ const matchingCommands = commandQuery === null ? [] : filterAgentCommands(commands, commandQuery);
352
+ useEffect(() => setActiveCommandIndex(0), [commandQuery]);
353
+ useEffect(() => {
354
+ if (commandQuery === null || commands.length > 0 || commandsLoading) return;
355
+ setCommandsLoading(true);
356
+ setCommandsError(null);
357
+ void handle.commands().then((result) => {
358
+ setCommands(result.commands);
359
+ setCommandsError(result.error);
360
+ }).catch((commandError: unknown) => {
361
+ setCommandsError(commandError instanceof Error ? commandError.message : String(commandError));
362
+ }).finally(() => setCommandsLoading(false));
363
+ }, [commandQuery, commands.length, commandsLoading, handle]);
364
+
365
+ const cancel = useCallback(async () => {
366
+ if (cancelling || liveStatus !== "running") return;
367
+ setCancelling(true);
368
+ setError(null);
369
+ try {
370
+ await cancelAgent({ agentId: agent.id });
371
+ setLiveStatus("idle");
372
+ setTimeout(() => void refresh(), 250);
373
+ } catch (cancelError) {
374
+ setError(cancelError instanceof Error ? cancelError.message : String(cancelError));
375
+ } finally {
376
+ setCancelling(false);
377
+ }
378
+ }, [agent.id, cancelAgent, cancelling, liveStatus, refresh]);
379
+
380
+ const rewind = useCallback(async (messageId: string, text: string, mode: "conversation" | "files" | "both") => {
381
+ if (rewindingMessageId || liveStatus === "running") return;
382
+ setRewindingMessageId(messageId);
383
+ setError(null);
384
+ try {
385
+ await rewindAgent({ agentId: agent.id, messageId, mode });
386
+ setEntries([]);
387
+ historyInitializedRef.current = false;
388
+ latestSeqRef.current = undefined;
389
+ setStartCursor(null);
390
+ setHasOlder(false);
391
+ if (mode !== "files") setPrompt(text);
392
+ await refresh();
393
+ } catch (rewindError) {
394
+ setError(rewindError instanceof Error ? rewindError.message : String(rewindError));
395
+ } finally {
396
+ setRewindingMessageId(null);
397
+ }
398
+ }, [agent.id, liveStatus, refresh, rewindAgent, rewindingMessageId]);
399
+
400
+ const colors = getColors(theme);
401
+ const composerStyles = useMemo(() => createComposerStyles(theme), [theme]);
402
+ const styles = useMemo(() => ({
403
+ ...composerStyles,
404
+ panel: {
405
+ height: "100%",
406
+ borderLeftWidth: 1,
407
+ borderLeftColor: colors.foregroundMuted + "22",
408
+ backgroundColor: colors.surface0,
409
+ } as ViewStyle,
410
+ header: {
411
+ height: 54,
412
+ paddingHorizontal: 14,
413
+ flexDirection: "row",
414
+ alignItems: "center",
415
+ gap: 8,
416
+ borderBottomWidth: 1,
417
+ borderBottomColor: colors.foregroundMuted + "22",
418
+ } as ViewStyle,
419
+ title: {
420
+ flex: 1,
421
+ color: colors.foreground,
422
+ fontSize: 14,
423
+ fontWeight: "600",
424
+ } as TextStyle,
425
+ subtitle: {
426
+ color: colors.foregroundMuted,
427
+ fontSize: 11,
428
+ } as TextStyle,
429
+ iconButton: {
430
+ width: 30,
431
+ height: 30,
432
+ alignItems: "center",
433
+ justifyContent: "center",
434
+ } as ViewStyle,
435
+ quickActionButton: {
436
+ width: 24,
437
+ height: 24,
438
+ borderRadius: 6,
439
+ borderWidth: 1,
440
+ borderColor: colors.foregroundMuted + "2f",
441
+ alignItems: "center",
442
+ justifyContent: "center",
443
+ } as ViewStyle,
444
+ toastCopyButton: {
445
+ height: 26,
446
+ paddingHorizontal: 8,
447
+ borderRadius: 6,
448
+ borderWidth: 1,
449
+ borderColor: colors.foregroundMuted + "2f",
450
+ flexDirection: "row",
451
+ alignItems: "center",
452
+ gap: 5,
453
+ } as ViewStyle,
454
+ autoCommitToast: {
455
+ marginHorizontal: 12,
456
+ marginBottom: 10,
457
+ paddingHorizontal: 10,
458
+ paddingVertical: 9,
459
+ borderRadius: 8,
460
+ borderWidth: 1,
461
+ borderColor: colors.statusDanger + "66",
462
+ backgroundColor: colors.surface0,
463
+ shadowColor: "#000",
464
+ shadowOpacity: 0.18,
465
+ shadowRadius: 14,
466
+ shadowOffset: { width: 0, height: 8 },
467
+ elevation: 20,
468
+ } as ViewStyle,
469
+ composerDock: {
470
+ paddingHorizontal: 12,
471
+ paddingTop: 10,
472
+ paddingBottom: 12,
473
+ borderTopWidth: 1,
474
+ borderTopColor: colors.foregroundMuted + "18",
475
+ backgroundColor: colors.surface0,
476
+ } as ViewStyle,
477
+ }), [colors, composerStyles]);
478
+
479
+ const autoCommitColor =
480
+ autoCommit.state.kind === "error"
481
+ ? colors.statusDanger
482
+ : autoCommit.state.kind === "done"
483
+ ? colors.statusSuccess
484
+ : autoCommit.state.kind === "running" || autoCommit.state.kind === "preparing"
485
+ ? colors.accent
486
+ : colors.foregroundMuted;
487
+
488
+ return (
489
+ <ResizeHandle
490
+ theme={theme}
491
+ side="left"
492
+ variant="grip"
493
+ initialWidth={initialPanelWidth}
494
+ minWidth={360}
495
+ maxWidth={maxPanelWidth}
496
+ style={styles.panel}
497
+ >
498
+ <View style={styles.header}>
499
+ <View style={{ flex: 1, minWidth: 0 }}>
500
+ <Text style={styles.title} numberOfLines={1}>{agent.title ?? agent.id.slice(0, 8)}</Text>
501
+ <Text style={styles.subtitle}>{agent.provider}{agent.model ? ` · ${agent.model}` : ""} · {liveStatus}</Text>
502
+ </View>
503
+ <QuickActionButton
504
+ icon={autoCommit.pending ? "LoaderCircle" : autoCommit.state.kind === "done" ? "CheckCircle2" : "GitCommitHorizontal"}
505
+ color={autoCommitColor}
506
+ disabled={autoCommit.pending || !commitCwd}
507
+ tooltip={[
508
+ { key: "Action", value: "提交代码" },
509
+ { key: "Status", value: autoCommit.state.kind === "idle" ? "Ready" : autoCommit.state.message },
510
+ { key: "Directory", value: commitCwd ?? "无工作目录" },
511
+ ]}
512
+ accessibilityLabel="提交当前 Agent 工作目录改动"
513
+ onPress={() => void autoCommit.start()}
514
+ style={styles.quickActionButton}
515
+ />
516
+ <Pressable accessibilityLabel="打开完整会话" onPress={onOpenFull} style={styles.iconButton}><Icon name="ExternalLink" color={colors.foregroundMuted} size={16} /></Pressable>
517
+ <Pressable accessibilityLabel="关闭会话预览" onPress={onClose} style={styles.iconButton}><Icon name="X" color={colors.foregroundMuted} size={16} /></Pressable>
518
+ </View>
519
+ {loading ? <Text style={{ color: colors.foregroundMuted, padding: 16 }}>加载会话中…</Text> : null}
520
+ {error ? <Text style={{ color: colors.statusDanger, padding: 16 }}>{error}</Text> : null}
521
+ {!error && lastAgentError ? <View accessibilityRole="alert" style={{ marginHorizontal: 12, marginTop: 10, padding: 10, borderRadius: 8, borderWidth: 1, borderColor: colors.statusDanger + "66", flexDirection: "row", gap: 8 }}><Icon name="CircleX" size={15} color={colors.statusDanger} /><Text selectable style={{ flex: 1, color: colors.statusDanger, fontSize: 12, lineHeight: 18 }}>{lastAgentError}</Text></View> : null}
522
+ <View style={{ flex: 1 }}>
523
+ <AgentConversationTimeline
524
+ entries={entries}
525
+ hasOlder={hasOlder}
526
+ loadingOlder={loadingOlder}
527
+ onLoadOlder={() => void loadOlder()}
528
+ scrollToEndVersion={scrollToEndVersion}
529
+ isRunning={liveStatus === "running"}
530
+ activeTurnStartedAt={activeTurnStartedAt}
531
+ pendingPermissions={pendingPermissions}
532
+ respondingRequestId={respondingRequestId}
533
+ onRespond={(requestId, response) => void respondToPermission(requestId, response)}
534
+ rewindCapabilities={handle.capabilities}
535
+ rewindingMessageId={rewindingMessageId}
536
+ onRewind={(messageId, text, mode) => void rewind(messageId, text, mode)}
537
+ theme={theme}
538
+ compact={compact}
539
+ />
540
+ {autoCommit.state.kind === "error" ? (
541
+ <AutoCommitErrorToast
542
+ message={autoCommit.state.message}
543
+ copied={autoCommit.copied}
544
+ colors={colors}
545
+ containerStyle={styles.autoCommitToast}
546
+ copyButtonStyle={styles.toastCopyButton}
547
+ onDismiss={autoCommit.dismiss}
548
+ onCopy={autoCommit.copyError}
549
+ />
550
+ ) : null}
551
+ <View style={styles.composerDock}>
552
+ {queuedMessages.length ? <View accessibilityLabel="已排队消息" style={{ gap: 6, marginBottom: 8 }}>
553
+ {queuedMessages.map((item, index) => <View key={item.id} style={{ flexDirection: "row", alignItems: "center", gap: 6, paddingHorizontal: 9, paddingVertical: 7, borderRadius: 7, backgroundColor: colors.surface0, borderWidth: 1, borderColor: colors.foregroundMuted + "33" }}>
554
+ <Text numberOfLines={2} style={{ flex: 1, color: colors.foregroundMuted, fontSize: 12 }}>{index + 1}. {item.text}</Text>
555
+ <Pressable accessibilityRole="button" accessibilityLabel="编辑排队消息" onPress={() => { setPrompt(item.text); updateQueuedMessages((current) => current.filter((candidate) => candidate.id !== item.id)); }} style={styles.iconButton}><Icon name="Pencil" size={13} color={colors.foregroundMuted} /></Pressable>
556
+ <Pressable accessibilityRole="button" accessibilityLabel="删除排队消息" onPress={() => updateQueuedMessages((current) => current.filter((candidate) => candidate.id !== item.id))} style={styles.iconButton}><Icon name="X" size={13} color={colors.foregroundMuted} /></Pressable>
557
+ </View>)}
558
+ </View> : null}
559
+ {commandQuery !== null && pendingPermissions.length === 0 ? <AgentCommandMenu
560
+ commands={commands}
561
+ query={commandQuery}
562
+ loading={commandsLoading}
563
+ error={commandsError}
564
+ theme={theme}
565
+ activeIndex={activeCommandIndex}
566
+ onSelect={(command) => setPrompt(`/${command.name} `)}
567
+ /> : null}
568
+ <SharedComposerInput
569
+ selection={composerSelection}
570
+ snapshot={providerSnapshot}
571
+ providerLoading={false}
572
+ openMenu={openMenu}
573
+ onToggleMenu={(menu) => setOpenMenu(menu === "model" || menu === "mode" || menu === "thinking" ? menu : null)}
574
+ onSelectModel={(combinedId) => void updateAgentConfig("model", combinedId.includes("::") ? combinedId.slice(combinedId.indexOf("::") + 2) : combinedId)}
575
+ onSelectMode={(modeId) => void updateAgentConfig("mode", modeId)}
576
+ onSelectThinking={(thinkingId) => void updateAgentConfig("thinking", thinkingId)}
577
+ isAgentRunning={liveStatus === "running"}
578
+ isCancelling={cancelling}
579
+ onCancel={() => void cancel()}
580
+ placeholder="继续跟进这个 Agent…"
581
+ theme={theme}
582
+ autoFocus
583
+ inputNativeID={`workspace-overview-agent-prompt-${agent.id}`}
584
+ disabled={sending || configPending || pendingPermissions.length > 0}
585
+ showAgentControls
586
+ pendingLabel={sending ? "Sending" : pendingPermissions.length > 0 ? "请先处理上方请求" : null}
587
+ value={prompt}
588
+ onChangeText={setPrompt}
589
+ onKeyPress={(event) => {
590
+ const key = event.nativeEvent.key;
591
+ if (key === "Enter") {
592
+ const nativeEvent = event.nativeEvent as typeof event.nativeEvent & { isComposing?: boolean; keyCode?: number };
593
+ if (nativeEvent.isComposing || nativeEvent.keyCode === 229) return;
594
+ event.preventDefault();
595
+ if (commandQuery === null) {
596
+ if (prompt.trim()) void submit();
597
+ return;
598
+ }
599
+ if (matchingCommands[activeCommandIndex]) {
600
+ setPrompt(`/${matchingCommands[activeCommandIndex].name} `);
601
+ }
602
+ return;
603
+ }
604
+ if (commandQuery === null) return;
605
+ if (key === "ArrowDown" || key === "ArrowUp") {
606
+ event.preventDefault();
607
+ if (matchingCommands.length) setActiveCommandIndex((current) => (current + (key === "ArrowDown" ? 1 : -1) + matchingCommands.length) % matchingCommands.length);
608
+ } else if (key === "Escape") {
609
+ event.preventDefault();
610
+ setPrompt("");
611
+ }
612
+ }}
613
+ onSubmit={submit}
614
+ usage={lastUsage}
615
+ />
616
+ </View>
617
+ </View>
618
+ </ResizeHandle>
619
+ );
620
+ }