Skip to content

Commit 2aa1630

Browse files
committed
refac
1 parent ae35bef commit 2aa1630

5 files changed

Lines changed: 70 additions & 9 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts">
2+
import MarkdownRenderer from '$lib/components/markdown/MarkdownRenderer.svelte';
3+
4+
let { content }: { content: string } = $props();
5+
</script>
6+
7+
<div class="h-full overflow-y-auto px-6 py-5">
8+
<div class="mx-auto max-w-3xl"><MarkdownRenderer {content} /></div>
9+
</div>

cptr/frontend/src/lib/components/chat/AssistantMessage.svelte

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import MessageTimestamp from './MessageTimestamp.svelte';
99
import ReasoningCollapsible from './ReasoningCollapsible.svelte';
1010
import ToolCallCollapsible from './ToolCallCollapsible.svelte';
11-
import { currentWorkspace, openFileTab } from '$lib/stores';
11+
import { currentWorkspace, openArtifactTab, openFileTab } from '$lib/stores';
1212
import { ttsConfigured, ttsEnabled } from '$lib/stores/audio';
1313
import { tooltip } from '$lib/tooltip';
1414
import { fileIconName } from '$lib/utils/fileIcon';
@@ -158,6 +158,10 @@
158158
collapsedFiles = { ...collapsedFiles, [key]: !collapsedFiles[key] };
159159
}
160160
161+
function openArtifact(artifact: any) {
162+
openArtifactTab(artifact.title || 'Artifact', artifact.content || '');
163+
}
164+
161165
/** Human-readable label for a tool call */
162166
function toolLabel(name: string, args: any): string {
163167
const _t = $t;
@@ -464,13 +468,7 @@
464468
hover:border-gray-300 dark:hover:border-white/12
465469
hover:bg-gray-50/50 dark:hover:bg-white/[0.03]
466470
transition-colors duration-150 {preview ? 'h-[4.375rem]' : 'h-[2.375rem]'}"
467-
onclick={() => {
468-
const ws = get(currentWorkspace);
469-
if (ws && artifact.path) {
470-
const fullPath = ws.path.replace(/\/$/, '') + '/' + artifact.path;
471-
openFileTab(fullPath);
472-
}
473-
}}
471+
onclick={() => openArtifact(artifact)}
474472
>
475473
<div class="h-full min-w-0 overflow-hidden px-3 py-2.5">
476474
<div

cptr/frontend/src/lib/stores.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,19 @@ export interface FileSearchTarget {
5353

5454
export interface Tab {
5555
id: string;
56-
type: 'home' | 'files' | 'terminal' | 'file' | 'git' | 'chat' | 'preview' | 'browser'; // preview is migrated on load
56+
type:
57+
| 'home'
58+
| 'files'
59+
| 'terminal'
60+
| 'file'
61+
| 'artifact'
62+
| 'git'
63+
| 'chat'
64+
| 'preview'
65+
| 'browser'; // preview is migrated on load
5766
label: string;
5867
filePath?: string;
68+
content?: string;
5969
edit?: boolean;
6070
path?: string; // generic path (e.g. for chat)
6171
sessionId?: string;
@@ -985,6 +995,28 @@ export function openFileTab(
985995
}));
986996
}
987997

998+
export function openArtifactTab(title: string, content: string): void {
999+
const workspace = get(currentWorkspace);
1000+
const state = workspace ?? get(homeState);
1001+
const group = state.groups.find((item) => item.id === state.activeGroupId);
1002+
if (!group) return;
1003+
1004+
const existing = group.tabs.find((tab) => tab.type === 'artifact' && tab.content === content);
1005+
const tab: Tab = existing ?? { id: nextId(), type: 'artifact', label: title, content };
1006+
const updateGroup = (group: EditorGroup) =>
1007+
group.id === state.activeGroupId
1008+
? { ...group, tabs: existing ? group.tabs : [...group.tabs, tab], activeTabId: tab.id }
1009+
: group;
1010+
1011+
if (workspace) {
1012+
currentWorkspace.update((current) =>
1013+
current ? { ...current, groups: current.groups.map(updateGroup) } : current
1014+
);
1015+
} else {
1016+
homeState.update((current) => ({ ...current, groups: current.groups.map(updateGroup) }));
1017+
}
1018+
}
1019+
9881020
export function openUntitledFileTab(targetGroupId?: string): void {
9891021
// Find the lowest unused number across ALL groups
9901022
const ws = get(currentWorkspace);

cptr/frontend/src/routes/+page.svelte

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import FileEditor from '$lib/components/FileEditor.svelte';
4444
import GitView from '$lib/components/GitView.svelte';
4545
import Terminal from '$lib/components/Terminal.svelte';
46+
import ArtifactViewer from '$lib/components/ArtifactViewer.svelte';
4647
import BrowserPreview from '$lib/components/BrowserPreview.svelte';
4748
import ChatPanel from '$lib/components/chat/ChatPanel.svelte';
4849
import DirectoryPicker from '$lib/components/DirectoryPicker.svelte';
@@ -1049,6 +1050,11 @@
10491050
/>
10501051
</div>
10511052
{/each}
1053+
{#each homePane.tabs.filter((tab) => tab.type === 'artifact') as tab (tab.id)}
1054+
<div class="persisted-tab" class:persisted-tab-hidden={tab.id !== homePane.activeTabId}>
1055+
<ArtifactViewer content={tab.content || ''} />
1056+
</div>
1057+
{/each}
10521058
{#each homePane.tabs.filter((tab) => tab.type === 'terminal' && tab.sessionId) as tab (tab.id)}
10531059
<div class="persisted-tab" class:persisted-tab-hidden={tab.id !== homePane.activeTabId}>
10541060
<Terminal sessionId={tab.sessionId!} />
@@ -1366,6 +1372,11 @@
13661372
/>
13671373
</div>
13681374
{/each}
1375+
{#each group.tabs.filter((tab) => tab.type === 'artifact') as tab (tab.id)}
1376+
<div class="persisted-tab" class:persisted-tab-hidden={tab.id !== group.activeTabId}>
1377+
<ArtifactViewer content={tab.content || ''} />
1378+
</div>
1379+
{/each}
13691380
{#each group.tabs.filter((tab) => tab.type === 'chat') as tab (tab.id)}
13701381
<div class="persisted-tab" class:persisted-tab-hidden={tab.id !== group.activeTabId}>
13711382
<ChatPanel

cptr/utils/chat_task.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,6 +2119,7 @@ async def _finish_reasoning_item():
21192119

21202120
restart = False
21212121
pending_calls: list[dict] = [] # Collect tool calls from this response
2122+
pending_call_ids: set[str] = set()
21222123
response_reasoning_items: list[dict] = [] # Pair with tool outputs on the next request
21232124
streamed_reasoning_chars = 0
21242125

@@ -2131,6 +2132,16 @@ async def _finish_reasoning_item():
21312132

21322133
elif event["type"] == "tool_call":
21332134
# Collect tool call — don't execute yet
2135+
call_id = event["call_id"]
2136+
if call_id in pending_call_ids:
2137+
logger.warning(
2138+
"[task %s] ignoring duplicate tool call id=%s name=%s",
2139+
message_id[:8],
2140+
call_id,
2141+
event["name"],
2142+
)
2143+
continue
2144+
pending_call_ids.add(call_id)
21342145
pending_calls.append(event)
21352146

21362147
elif event["type"] in ("output", "reasoning"):

0 commit comments

Comments
 (0)