chore(integration): snapshot local main worktree
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Bot,
|
||||
ChevronRight,
|
||||
CircleAlert,
|
||||
LoaderCircle,
|
||||
MessageSquarePlus,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -16,8 +13,10 @@ import {
|
||||
uploadCodingAttachment,
|
||||
type CodingAttachmentRef,
|
||||
} from '@/lib/coding-attachments';
|
||||
import { forkCodingConversation } from '@/lib/coding-conversations';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
abortCodingConversation,
|
||||
forkCodingConversation,
|
||||
} from '@/lib/coding-conversations';
|
||||
import {
|
||||
codingConversationStore,
|
||||
selectCodingConversationDraft,
|
||||
@@ -35,10 +34,11 @@ import type {
|
||||
CodingConversationMetadata,
|
||||
} from '@/types/coding-project';
|
||||
import { CodingComposer } from './CodingComposer';
|
||||
import { CodingChangesSummary } from './CodingChangesSummary';
|
||||
import { CodingConversationSidebar } from './CodingConversationSidebar';
|
||||
import { CodingConversationHeader } from './CodingConversationHeader';
|
||||
import { CodingConversationTimeline } from './CodingConversationTimeline';
|
||||
import { CodingInteractionPanel } from './CodingInteractionPanel';
|
||||
import { CodingWorkspaceInspector } from './CodingWorkspaceInspector';
|
||||
import { createLocalConversationSnapshot } from './coding-chat-snapshot';
|
||||
|
||||
export interface CodingChatPanelProps {
|
||||
@@ -126,10 +126,8 @@ export function CodingChatPanel({
|
||||
const createConversation = useCodingWorkspaceStore((state) => state.createConversation);
|
||||
const patchConversation = useCodingWorkspaceStore((state) => state.patchConversation);
|
||||
const upsertConversation = useCodingWorkspaceStore((state) => state.upsertConversation);
|
||||
const acknowledgeLegacyNotice = useCodingWorkspaceStore((state) => state.acknowledgeLegacyNotice);
|
||||
|
||||
const selectedConversationId = useCodingConversationStore((state) => state.selectedConversationId);
|
||||
const connectionState = useCodingConversationStore((state) => state.connectionState);
|
||||
const connectionError = useCodingConversationStore((state) => state.globalError);
|
||||
const primeConversation = useCodingConversationStore((state) => state.primeConversation);
|
||||
const selectConversation = useCodingConversationStore((state) => state.selectConversation);
|
||||
@@ -145,7 +143,6 @@ export function CodingChatPanel({
|
||||
const [provisionalDrafts, setProvisionalDrafts] = useState<Record<string, string>>({});
|
||||
const [submissionErrors, setSubmissionErrors] = useState<Record<string, LocalSubmissionError>>({});
|
||||
const [modesByDraftKey, setModesByDraftKey] = useState<Record<string, PromptMode>>({});
|
||||
const [inspectorOpen, setInspectorOpen] = useState(false);
|
||||
const [attachmentsByDraftKey, setAttachmentsByDraftKey] = useState<
|
||||
Record<string, LocalComposerAttachment[]>
|
||||
>({});
|
||||
@@ -160,13 +157,6 @@ export function CodingChatPanel({
|
||||
config?.agents.filter((agent) => agent.enabled && !agent.archivedAt) ?? []
|
||||
), [config]);
|
||||
const selectedAgent = agents.find((agent) => agent.id === selectedAgentId) ?? null;
|
||||
const agentConversations = useMemo(() => (
|
||||
selectedAgent
|
||||
? conversations
|
||||
.filter((conversation) => conversation.agentId === selectedAgent.id && !conversation.archivedAt)
|
||||
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
||||
: []
|
||||
), [conversations, selectedAgent]);
|
||||
const selectedConversation = conversations.find((conversation) => (
|
||||
conversation.id === selectedConversationId
|
||||
&& conversation.agentId === selectedAgent?.id
|
||||
@@ -571,7 +561,7 @@ export function CodingChatPanel({
|
||||
</div>
|
||||
<h1 className="mt-4 text-balance text-lg font-semibold">先选择一个编程项目</h1>
|
||||
<p className="mt-2 text-pretty text-sm leading-6 text-muted-foreground">
|
||||
项目和伙伴信息只在本地读取,不会为了打开输入框启动 Agent。
|
||||
项目与智能体配置从本地读取,打开输入框不会提前启动 Pi 运行实例。
|
||||
</p>
|
||||
<Button className="mt-5 min-h-10 rounded-xl" onClick={onOpenProjectSettings}>
|
||||
打开项目设置
|
||||
@@ -583,167 +573,36 @@ export function CodingChatPanel({
|
||||
|
||||
return (
|
||||
<section className="flex min-h-0 flex-1 overflow-hidden bg-background text-foreground" data-testid="coding-chat-panel">
|
||||
<aside className="hidden w-60 shrink-0 flex-col border-r border-foreground/10 bg-surface-subtle/45 md:flex">
|
||||
<div className="border-b border-foreground/10 px-4 py-4">
|
||||
<p className="truncate text-sm font-semibold">{activeProject?.name ?? 'Makelore Code'}</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">本地 Conversation</p>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-2">
|
||||
<p className="px-2 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">伙伴与对话</p>
|
||||
<div className="space-y-1.5">
|
||||
{agents.map((agent) => {
|
||||
const expanded = selectedAgent?.id === agent.id;
|
||||
const conversationGroupId = `agent-conversations-${agent.id}`;
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className={cn(
|
||||
'rounded-2xl',
|
||||
expanded && 'bg-background p-1 shadow-soft',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex min-h-10 w-full items-center gap-2 rounded-xl px-2.5 text-left text-sm transition-[background-color,scale] duration-150 ease-out active:scale-[0.96]',
|
||||
expanded ? 'font-semibold' : 'hover:bg-background/70',
|
||||
)}
|
||||
aria-label={agent.name}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={conversationGroupId}
|
||||
onClick={() => {
|
||||
clearConversationSelection();
|
||||
selectAgent(agent.id);
|
||||
}}
|
||||
>
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-foreground text-xs font-semibold text-background" aria-hidden="true">
|
||||
{agent.name.trim().slice(0, 1) || 'A'}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">{agent.name}</span>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out',
|
||||
expanded && 'rotate-90',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div
|
||||
id={conversationGroupId}
|
||||
role="group"
|
||||
aria-label={`${agent.name} 的对话`}
|
||||
className="mb-1 ml-3 mt-1 border-l border-foreground/10 pl-3 pr-1"
|
||||
>
|
||||
<div className="flex min-h-10 items-center justify-between pl-2">
|
||||
<p className="min-w-0 truncate text-[11px] font-semibold text-muted-foreground">
|
||||
{agent.name} 的对话
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10 rounded-xl transition-transform duration-150 ease-out active:scale-[0.96]"
|
||||
aria-label="新建对话"
|
||||
disabled={Boolean(creatingAgentIds[agent.id])}
|
||||
onClick={() => void handleCreateConversation()}
|
||||
>
|
||||
{creatingAgentIds[agent.id]
|
||||
? <LoaderCircle className="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
: <MessageSquarePlus className="h-4 w-4" aria-hidden="true" />}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1 pb-1">
|
||||
{agentConversations.map((conversation) => {
|
||||
const unread = conversationSummaries[conversation.id]?.unread ?? conversation.unread;
|
||||
const summary = conversationSummaries[conversation.id];
|
||||
return (
|
||||
<button
|
||||
key={conversation.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex min-h-10 w-full items-center gap-2 rounded-xl px-3 text-left text-sm transition-[background-color,scale] duration-150 ease-out active:scale-[0.96]',
|
||||
targetConversationId === conversation.id
|
||||
? 'bg-surface-subtle font-medium'
|
||||
: 'text-muted-foreground hover:bg-surface-subtle/70 hover:text-foreground',
|
||||
)}
|
||||
onClick={() => handleSelectConversation(conversation)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{conversation.title}</span>
|
||||
{summary && ['queued', 'running', 'retrying', 'compacting', 'aborting'].includes(summary.runStatus) && (
|
||||
<LoaderCircle className="h-3.5 w-3.5 shrink-0 animate-spin text-brand" aria-label="对话正在运行" />
|
||||
)}
|
||||
{unread && <span className="h-2 w-2 shrink-0 rounded-full bg-brand" aria-label="未读" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{autoCreating && (
|
||||
<div className="flex min-h-10 items-center gap-2 px-3 text-xs text-muted-foreground">
|
||||
<LoaderCircle className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
|
||||
正在创建首个对话…
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<CodingConversationSidebar
|
||||
projectName={activeProject?.name}
|
||||
agents={agents}
|
||||
conversations={conversations}
|
||||
conversationSummaries={conversationSummaries}
|
||||
selectedAgentId={selectedAgent?.id ?? null}
|
||||
selectedConversationId={targetConversationId}
|
||||
creatingAgentIds={creatingAgentIds}
|
||||
onSelectAgent={(agentId) => {
|
||||
clearConversationSelection();
|
||||
selectAgent(agentId);
|
||||
}}
|
||||
onSelectConversation={handleSelectConversation}
|
||||
onCreateConversation={() => void handleCreateConversation()}
|
||||
onOpenProjectSettings={onOpenProjectSettings}
|
||||
/>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<CodingConversationHeader
|
||||
key={`header:${targetConversationId ?? 'none'}`}
|
||||
conversation={selectedConversation}
|
||||
snapshot={snapshot}
|
||||
connectionState={connectionState}
|
||||
onRename={async (title) => {
|
||||
if (targetConversationId) await patchConversation(targetConversationId, { title });
|
||||
}}
|
||||
onArchive={async () => {
|
||||
if (!targetConversationId) return;
|
||||
const sourceConversationId = targetConversationId;
|
||||
await patchConversation(sourceConversationId, { archived: true });
|
||||
if (codingConversationStore.getState().selectedConversationId === sourceConversationId) {
|
||||
clearConversationSelection();
|
||||
}
|
||||
}}
|
||||
onToggleUnread={async () => {
|
||||
if (!targetConversationId || !selectedConversation) return;
|
||||
const unread = !selectedConversation.unread;
|
||||
await patchConversation(targetConversationId, { unread });
|
||||
markConversationUnread(targetConversationId, unread);
|
||||
}}
|
||||
onFork={() => handleForkConversation()}
|
||||
onRefresh={async () => {
|
||||
if (targetConversationId) await loadConversationSnapshot(targetConversationId, true);
|
||||
}}
|
||||
onRecover={async () => {
|
||||
if (targetConversationId) await recoverConversation(targetConversationId);
|
||||
}}
|
||||
onOpenInspector={() => setInspectorOpen(true)}
|
||||
onOpenSettings={onOpenProjectSettings}
|
||||
/>
|
||||
|
||||
{config?.legacyConversationNotice === 'pending' && (
|
||||
<div className="mx-4 mt-3 flex min-h-10 items-center gap-3 rounded-xl border border-amber-300/70 bg-amber-50 px-3 py-2 text-xs text-amber-950 sm:mx-5" data-testid="legacy-conversation-notice">
|
||||
<CircleAlert className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
<p className="min-w-0 flex-1 text-pretty">
|
||||
旧版 OpenCode 对话不能在 Pi 中继续;原数据已保留,仅用于旧版本回滚或导出。
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="min-h-10 shrink-0 rounded-xl px-3 text-amber-950"
|
||||
onClick={() => void acknowledgeLegacyNotice()}
|
||||
>
|
||||
知道了
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(workspaceError || conversationMetadataError || connectionError) && (
|
||||
<div className="mx-4 mt-3 flex min-h-10 items-center gap-2 rounded-xl bg-destructive/5 px-3 py-2 text-xs text-destructive sm:mx-5">
|
||||
<CircleAlert className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
@@ -769,6 +628,7 @@ export function CodingChatPanel({
|
||||
<CodingConversationTimeline
|
||||
key={targetConversationId}
|
||||
conversationId={targetConversationId}
|
||||
assistantName={selectedAgent?.name}
|
||||
onFork={(sourceEntryId) => {
|
||||
void handleForkConversation(sourceEntryId).catch((error) => {
|
||||
if (!draftKey) return;
|
||||
@@ -789,7 +649,15 @@ export function CodingChatPanel({
|
||||
key={`interaction:${targetConversationId}`}
|
||||
conversationId={targetConversationId}
|
||||
interactions={snapshot?.pendingInteractions ?? []}
|
||||
onSettled={() => loadConversationSnapshot(targetConversationId, true).then(() => undefined)}
|
||||
onSettled={() => loadConversationSnapshot(targetConversationId, 'silent').then(() => undefined)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{targetConversationId && (
|
||||
<CodingChangesSummary
|
||||
key={`changes:${targetConversationId}`}
|
||||
conversationId={targetConversationId}
|
||||
snapshot={snapshot}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -807,7 +675,10 @@ export function CodingChatPanel({
|
||||
acceptedCount={acceptedRequestCount}
|
||||
attachments={localAttachments.map(({ id, name, previewUrl }) => ({ id, name, previewUrl }))}
|
||||
submitting={submittingRequestCount > 0}
|
||||
placeholder={selectedAgent ? '给当前对话发送消息' : '请选择一个伙伴后再发送'}
|
||||
placeholder={selectedAgent ? '随心输入' : '请选择一个智能体后再发送'}
|
||||
conversation={selectedConversation}
|
||||
snapshot={snapshot}
|
||||
voiceScopeKey={draftKey}
|
||||
onModeChange={(mode) => {
|
||||
if (draftKey) setModesByDraftKey((current) => ({ ...current, [draftKey]: mode }));
|
||||
}}
|
||||
@@ -825,6 +696,16 @@ export function CodingChatPanel({
|
||||
}
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
onAbort={() => {
|
||||
if (!targetConversationId) return;
|
||||
void abortCodingConversation(targetConversationId).catch((error) => {
|
||||
if (!draftKey) return;
|
||||
setSubmissionErrors((current) => ({
|
||||
...current,
|
||||
[draftKey]: localSubmissionError(error),
|
||||
}));
|
||||
});
|
||||
}}
|
||||
onRecover={() => {
|
||||
if (targetConversationId) {
|
||||
void recoverConversation(targetConversationId).catch(() => undefined);
|
||||
@@ -832,18 +713,8 @@ export function CodingChatPanel({
|
||||
}}
|
||||
onAddFiles={handleAddFiles}
|
||||
onRemoveAttachment={handleRemoveAttachment}
|
||||
/>
|
||||
<CodingWorkspaceInspector
|
||||
key={`inspector:${targetConversationId ?? 'none'}`}
|
||||
open={inspectorOpen}
|
||||
onOpenChange={setInspectorOpen}
|
||||
conversationId={targetConversationId}
|
||||
agentId={selectedAgent?.id ?? null}
|
||||
snapshot={snapshot}
|
||||
onUseCommand={(command) => {
|
||||
if (!targetConversationId) return;
|
||||
const current = codingConversationStore.getState().draftsByConversationId[targetConversationId]?.text ?? '';
|
||||
setConversationDraft(targetConversationId, current ? `${current}\n${command}` : command);
|
||||
onRefreshRuntime={async () => {
|
||||
if (targetConversationId) await loadConversationSnapshot(targetConversationId, 'silent');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user