repoburg 1.3.199 → 1.3.202
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/agent-pane/backend/data.db +0 -0
- package/agent-pane/backend/data.db-shm +0 -0
- package/agent-pane/backend/data.db-wal +0 -0
- package/agent-pane/backend/dist/index.js +1658 -277
- package/agent-pane/backend/src/claude/claude-provider.test.ts +393 -3
- package/agent-pane/backend/src/claude/claude-provider.ts +53 -50
- package/agent-pane/backend/src/db/index.ts +12 -0
- package/agent-pane/backend/src/index.ts +5 -2
- package/agent-pane/backend/src/integration/threads-claude-fork.test.ts +241 -0
- package/agent-pane/backend/src/providers/index.ts +3 -1
- package/agent-pane/backend/src/providers/runtime-provider.ts +6 -0
- package/agent-pane/backend/src/repoburg/action-mapping.test.ts +122 -0
- package/agent-pane/backend/src/repoburg/action-mapping.ts +294 -0
- package/agent-pane/backend/src/repoburg/agent-runtime-client.test.ts +271 -0
- package/agent-pane/backend/src/repoburg/agent-runtime-client.ts +285 -0
- package/agent-pane/backend/src/repoburg/backend-resolver.test.ts +356 -0
- package/agent-pane/backend/src/repoburg/backend-resolver.ts +261 -0
- package/agent-pane/backend/src/repoburg/repoburg-client.test.ts +180 -0
- package/agent-pane/backend/src/repoburg/repoburg-client.ts +194 -0
- package/agent-pane/backend/src/repoburg/repoburg-provider.test.ts +780 -0
- package/agent-pane/backend/src/repoburg/repoburg-provider.ts +471 -0
- package/agent-pane/backend/src/repoburg/turn-projector.test.ts +621 -0
- package/agent-pane/backend/src/repoburg/turn-projector.ts +637 -0
- package/agent-pane/backend/src/routes/providers.test.ts +18 -1
- package/agent-pane/backend/src/routes/providers.ts +19 -1
- package/agent-pane/backend/src/routes/sessions.test.ts +215 -1
- package/agent-pane/backend/src/routes/sessions.ts +82 -9
- package/agent-pane/backend/src/routes/threads.ts +21 -6
- package/agent-pane/backend/src/routes/turns.test.ts +45 -1
- package/agent-pane/frontend/dist/assets/index-CJxD4YmJ.js +370 -0
- package/agent-pane/frontend/dist/assets/{index-Dwwhx7YN.css → index-r-xwfOKQ.css} +1 -1
- package/agent-pane/frontend/dist/index.html +2 -2
- package/agent-pane/frontend/src/api/sessions.test.ts +1 -0
- package/agent-pane/frontend/src/api/websocket.test.ts +2 -2
- package/agent-pane/frontend/src/api/websocket.ts +3 -1
- package/agent-pane/frontend/src/components/AskUserItem.test.tsx +143 -0
- package/agent-pane/frontend/src/components/AskUserItem.tsx +207 -0
- package/agent-pane/frontend/src/components/CommentableText.test.tsx +196 -0
- package/agent-pane/frontend/src/components/CommentableText.tsx +109 -21
- package/agent-pane/frontend/src/components/IdeWorkspace.test.tsx +39 -0
- package/agent-pane/frontend/src/components/IdeWorkspace.tsx +28 -2
- package/agent-pane/frontend/src/components/ResizeHandle.tsx +29 -0
- package/agent-pane/frontend/src/components/TurnContent.test.tsx +79 -0
- package/agent-pane/frontend/src/components/TurnContent.tsx +49 -3
- package/agent-pane/frontend/src/components/panels/ThreadsPanel.tsx +35 -3
- package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +48 -0
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +7 -2
- package/agent-pane/frontend/src/constants/models.ts +37 -0
- package/agent-pane/frontend/src/hooks/useResizable.test.ts +97 -0
- package/agent-pane/frontend/src/hooks/useResizable.ts +75 -0
- package/agent-pane/frontend/src/store/store.test.ts +54 -0
- package/agent-pane/frontend/src/store/uiSlice.ts +50 -0
- package/agent-pane/frontend/src/store/wsSlice.ts +10 -3
- package/agent-pane/frontend/src/styles/app.css +26 -1
- package/agent-pane/frontend/src/test/component-helpers.ts +2 -0
- package/agent-pane/frontend/src/test/fixtures.ts +1 -0
- package/agent-pane/frontend/src/types/index.ts +2 -0
- package/agent-pane/frontend/src/utils/duration.test.ts +1 -0
- package/agent-pane/shared/types.ts +17 -0
- package/agent-pane-bug-repro-no-agent-reply.png +0 -0
- package/askuser-after-step5-postgres-selected.png +0 -0
- package/askuser-after-step7-answer-sent.png +0 -0
- package/askuser-card-visible.png +0 -0
- package/backend/dist/src/agent-runtime/agent-runtime-event-emitter.d.ts +36 -0
- package/backend/dist/src/agent-runtime/agent-runtime-event-emitter.js +199 -0
- package/backend/dist/src/agent-runtime/agent-runtime-event-emitter.js.map +1 -0
- package/backend/dist/src/agent-runtime/agent-runtime.controller.d.ts +11 -0
- package/backend/dist/src/agent-runtime/agent-runtime.controller.js +91 -0
- package/backend/dist/src/agent-runtime/agent-runtime.controller.js.map +1 -0
- package/backend/dist/src/agent-runtime/agent-runtime.module.d.ts +2 -0
- package/backend/dist/src/agent-runtime/agent-runtime.module.js +26 -0
- package/backend/dist/src/agent-runtime/agent-runtime.module.js.map +1 -0
- package/backend/dist/src/agent-runtime/agent-runtime.types.d.ts +92 -0
- package/backend/dist/src/agent-runtime/agent-runtime.types.js +3 -0
- package/backend/dist/src/agent-runtime/agent-runtime.types.js.map +1 -0
- package/backend/dist/src/app.controller.d.ts +1 -0
- package/backend/dist/src/app.module.js +4 -0
- package/backend/dist/src/app.module.js.map +1 -1
- package/backend/dist/src/app.service.d.ts +1 -0
- package/backend/dist/src/app.service.js +4 -1
- package/backend/dist/src/app.service.js.map +1 -1
- package/backend/dist/src/interactive-chat/chat.module.js +4 -0
- package/backend/dist/src/interactive-chat/chat.module.js.map +1 -1
- package/backend/dist/src/interactive-chat/chat.service.d.ts +7 -1
- package/backend/dist/src/interactive-chat/chat.service.js +54 -7
- package/backend/dist/src/interactive-chat/chat.service.js.map +1 -1
- package/backend/dist/src/llm-orchestration/action-handlers/invoke-subagent.handler.js +12 -1
- package/backend/dist/src/llm-orchestration/action-handlers/invoke-subagent.handler.js.map +1 -1
- package/backend/dist/src/llm-orchestration/hooks/frontend-notification.hook.d.ts +3 -1
- package/backend/dist/src/llm-orchestration/hooks/frontend-notification.hook.js +20 -2
- package/backend/dist/src/llm-orchestration/hooks/frontend-notification.hook.js.map +1 -1
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.d.ts +1 -0
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.js +1 -0
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.js.map +1 -1
- package/backend/dist/src/llm-orchestration/llm-orchestration.module.js +2 -0
- package/backend/dist/src/llm-orchestration/llm-orchestration.module.js.map +1 -1
- package/backend/dist/src/llm-orchestration/llm-turn-processor.service.d.ts +4 -1
- package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js +37 -2
- package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js.map +1 -1
- package/backend/dist/src/llm-provider/ollama-llm.provider.d.ts +1 -0
- package/backend/dist/src/llm-provider/ollama-llm.provider.js +19 -1
- package/backend/dist/src/llm-provider/ollama-llm.provider.js.map +1 -1
- package/backend/dist/src/llm-responses/llm-responses.module.js +4 -0
- package/backend/dist/src/llm-responses/llm-responses.module.js.map +1 -1
- package/backend/dist/src/llm-responses/llm-responses.service.d.ts +4 -1
- package/backend/dist/src/llm-responses/llm-responses.service.js +52 -2
- package/backend/dist/src/llm-responses/llm-responses.service.js.map +1 -1
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/daemon/dist/api/services.js +4 -0
- package/daemon/dist/index.js +0 -0
- package/daemon/dist/serviceManager.js +30 -11
- package/daemon/dist/serviceManager.spec.js +26 -0
- package/daemon/dist/workspace-validation.js +63 -0
- package/daemon/dist/workspace-validation.spec.js +63 -0
- package/package.json +1 -1
- package/r2-step4-apple-banana.png +0 -0
- package/r2-step5-after-rewind-apple-last.png +0 -0
- package/r2-step7-apple-cherry.png +0 -0
- package/r2-step8-cherry-menu-no-restore.png +0 -0
- package/r3-step3-live-task-card-running.png +0 -0
- package/r3-step4-after-completion.png +0 -0
- package/r3-step5-persistence-after-reload.png +0 -0
- package/r3-step6-tasks-tab-card.png +0 -0
- package/reverify-answered-state-answer-sent.png +0 -0
- package/reverify-card-visible-after-completion.png +0 -0
- package/rewind-step3-apple-banana-turns.png +0 -0
- package/rewind-step5-ui-unchanged-after-confirmed-rewind.png +0 -0
- package/rewind-step6-apple-menu-no-restore.png +0 -0
- package/rewind-step7-final-turnlist-unchanged.png +0 -0
- package/rewind-step7-negative-error-toast.png +0 -0
- package/rewind-step7-negative-error-toast2.png +0 -0
- package/subagent-r2-step5-turn-after-reload-no-card.png +0 -0
- package/subagent-r2-step6-tasks-panel-card.png +0 -0
- package/subagent-step3-tasks-tab-done-card.png +0 -0
- package/subagent-step4-turn-stuck-no-cards.png +0 -0
- package/agent-pane/frontend/dist/assets/index-BYo-lnxm.js +0 -362
|
@@ -9,6 +9,7 @@ import type { Turn, Item, DiscussionThread, Attachment } from '../types/index.ts
|
|
|
9
9
|
import { runtimeIcon } from '../types/index.ts';
|
|
10
10
|
import CollapsibleToolCalls, { isToolItem } from './CollapsibleToolCalls.tsx';
|
|
11
11
|
import CommentableText from './CommentableText.tsx';
|
|
12
|
+
import AskUserItem from './AskUserItem.tsx';
|
|
12
13
|
|
|
13
14
|
marked.setOptions({ breaks: true, gfm: true });
|
|
14
15
|
|
|
@@ -306,6 +307,10 @@ function ItemCard({ item, runtimeType, turn, threads }: { item: Item; runtimeTyp
|
|
|
306
307
|
);
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
case 'ask_user': {
|
|
311
|
+
return <AskUserItem item={item} turn={turn} />;
|
|
312
|
+
}
|
|
313
|
+
|
|
309
314
|
case 'background_task': {
|
|
310
315
|
const data = item.data || {};
|
|
311
316
|
const taskType = (data.task_type as string) || 'unknown';
|
|
@@ -427,15 +432,35 @@ function TurnContentBase({ turn, runtimeType, threads = [] }: Props) {
|
|
|
427
432
|
// Intermediate = everything before final agent_message (or ALL items if running)
|
|
428
433
|
const intermediateItems = hasFinalResponse ? turn.items.slice(0, lastAgentIdx) : turn.items;
|
|
429
434
|
|
|
435
|
+
// Fallback final response — some runtime turns (repoburg final-action
|
|
436
|
+
// rounds) never produce an agent_message item; their final text lives on
|
|
437
|
+
// turn.agent_response instead. Synthesize the final card from it.
|
|
438
|
+
const responseFallback: Item | null =
|
|
439
|
+
!hasFinalResponse && turn.status === 'completed' && turn.agent_response
|
|
440
|
+
? {
|
|
441
|
+
id: `response-${turn.id}`,
|
|
442
|
+
turn_id: turn.id,
|
|
443
|
+
external_item_id: `response-${turn.id}`,
|
|
444
|
+
type: 'agent_message',
|
|
445
|
+
data: { text: turn.agent_response },
|
|
446
|
+
status: 'completed',
|
|
447
|
+
created_at: turn.created_at,
|
|
448
|
+
updated_at: turn.updated_at,
|
|
449
|
+
}
|
|
450
|
+
: null;
|
|
451
|
+
|
|
430
452
|
// Filter out empty reasoning items (text is "" — signed-empty thinking blocks from Claude SDK).
|
|
431
453
|
// These inflate the intermediate step count and render as empty cards.
|
|
432
454
|
const visibleIntermediate = intermediateItems.filter(
|
|
433
455
|
(item) => !(item.type === 'reasoning' && (!item.data?.text || item.data.text === '')),
|
|
434
456
|
);
|
|
435
457
|
|
|
436
|
-
// Filter main flow: remove background_task items (they belong in Tasks panel)
|
|
458
|
+
// Filter main flow: remove background_task items (they belong in Tasks panel)
|
|
459
|
+
// and ask_user items (always-visible section below — the answer flow must
|
|
460
|
+
// never hide behind the collapsed intermediate section).
|
|
437
461
|
const mainIntermediate = visibleIntermediate.filter((item) => {
|
|
438
462
|
if (item.type === 'background_task') return false;
|
|
463
|
+
if (item.type === 'ask_user') return false;
|
|
439
464
|
return true;
|
|
440
465
|
});
|
|
441
466
|
|
|
@@ -517,11 +542,32 @@ function TurnContentBase({ turn, runtimeType, threads = [] }: Props) {
|
|
|
517
542
|
</div>
|
|
518
543
|
)}
|
|
519
544
|
|
|
545
|
+
{/* background_task cards — always visible in the turn flow (live sub-agent
|
|
546
|
+
/ monitor lifecycle), outside the collapsible intermediate section.
|
|
547
|
+
Rendered before the final answer to match the chronological flow
|
|
548
|
+
(task ran, then the agent responded). Mirrors the Tasks panel entries
|
|
549
|
+
but inline with the conversation. */}
|
|
550
|
+
{turn.items.filter((i) => i.type === 'background_task').map((item) => (
|
|
551
|
+
<MemoItemCard key={`task-${item.id}`} item={item} runtimeType={runtimeType} turn={turn} threads={threads} />
|
|
552
|
+
))}
|
|
553
|
+
|
|
520
554
|
{/* Final agent message — always visible */}
|
|
521
555
|
{hasFinalResponse && <MemoItemCard key={turn.items[lastAgentIdx].id} item={turn.items[lastAgentIdx]} runtimeType={runtimeType} turn={turn} threads={threads} />}
|
|
522
556
|
|
|
523
|
-
{/*
|
|
524
|
-
{
|
|
557
|
+
{/* Fallback final response (turn.agent_response with no agent_message item). */}
|
|
558
|
+
{responseFallback && (
|
|
559
|
+
<MemoItemCard key={responseFallback.id} item={responseFallback} runtimeType={runtimeType} turn={turn} threads={threads} />
|
|
560
|
+
)}
|
|
561
|
+
|
|
562
|
+
{/* Items after final agent message (rare — errors, etc.; ask_user and
|
|
563
|
+
background_task render in the dedicated always-visible sections below). */}
|
|
564
|
+
{hasFinalResponse && turn.items.slice(lastAgentIdx + 1).filter((i) => i.type !== 'ask_user' && i.type !== 'background_task').map((item) => <MemoItemCard key={item.id} item={item} runtimeType={runtimeType} turn={turn} threads={threads} />)}
|
|
565
|
+
|
|
566
|
+
{/* ask_user cards — always visible + interactive, outside the collapsible
|
|
567
|
+
intermediate section (works even for ask_user-only turns). */}
|
|
568
|
+
{turn.items.filter((i) => i.type === 'ask_user').map((item) => (
|
|
569
|
+
<MemoItemCard key={`ask-${item.id}`} item={item} runtimeType={runtimeType} turn={turn} threads={threads} />
|
|
570
|
+
))}
|
|
525
571
|
|
|
526
572
|
{/* Pending state (no items at all) */}
|
|
527
573
|
{turn.items.length === 0 && !turn.error && (turn.status === 'pending' || turn.status === 'running') && (
|
|
@@ -98,7 +98,7 @@ function ThreadCard({
|
|
|
98
98
|
setShowQuoteBtn(false);
|
|
99
99
|
}, []);
|
|
100
100
|
|
|
101
|
-
const
|
|
101
|
+
const checkSelection = useCallback(() => {
|
|
102
102
|
const container = conversationRef.current;
|
|
103
103
|
if (!container) return;
|
|
104
104
|
const sel = window.getSelection();
|
|
@@ -128,13 +128,46 @@ function ThreadCard({
|
|
|
128
128
|
const selRect = range.getBoundingClientRect();
|
|
129
129
|
const BUTTON_HEIGHT = 28;
|
|
130
130
|
const GAP = 6;
|
|
131
|
+
const BUTTON_WIDTH_EST = 170;
|
|
132
|
+
|
|
133
|
+
// Detect backward selection via anchor (press) vs focus (release) order.
|
|
134
|
+
let backward = false;
|
|
135
|
+
if (sel.anchorNode && sel.focusNode) {
|
|
136
|
+
try {
|
|
137
|
+
const fr = document.createRange();
|
|
138
|
+
fr.setStart(sel.focusNode, sel.focusOffset);
|
|
139
|
+
fr.setEnd(sel.focusNode, sel.focusOffset);
|
|
140
|
+
const ar = document.createRange();
|
|
141
|
+
ar.setStart(sel.anchorNode, sel.anchorOffset);
|
|
142
|
+
ar.setEnd(sel.anchorNode, sel.anchorOffset);
|
|
143
|
+
backward = ar.compareBoundaryPoints(Range.START_TO_START, fr) > 0;
|
|
144
|
+
} catch {
|
|
145
|
+
backward = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
let btnY = selRect.top - BUTTON_HEIGHT - GAP;
|
|
132
150
|
if (btnY < 8) btnY = selRect.bottom + GAP;
|
|
133
|
-
|
|
151
|
+
let btnX = backward ? selRect.left - BUTTON_WIDTH_EST : selRect.right;
|
|
152
|
+
btnX = Math.max(8, Math.min(btnX, window.innerWidth - BUTTON_WIDTH_EST - 8));
|
|
153
|
+
setQuoteBtnPos({ x: btnX, y: btnY });
|
|
134
154
|
setSelectedText(text);
|
|
135
155
|
setShowQuoteBtn(true);
|
|
136
156
|
}, []);
|
|
137
157
|
|
|
158
|
+
// Detect selections on mouseup anywhere in the document so that releasing
|
|
159
|
+
// the mouse in the surrounding padding (common with backward drags) still
|
|
160
|
+
// registers the quote button.
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
function onDocMouseUp(e: MouseEvent) {
|
|
163
|
+
const t = e.target as HTMLElement | null;
|
|
164
|
+
if (t && t.closest('.selection-actions')) return;
|
|
165
|
+
checkSelection();
|
|
166
|
+
}
|
|
167
|
+
document.addEventListener('mouseup', onDocMouseUp);
|
|
168
|
+
return () => document.removeEventListener('mouseup', onDocMouseUp);
|
|
169
|
+
}, [checkSelection]);
|
|
170
|
+
|
|
138
171
|
const handleQuoteClick = useCallback(() => {
|
|
139
172
|
const formatted = `\n\`\`\`QUOTE\n${selectedText}\n\`\`\`\n\n`;
|
|
140
173
|
setFollowup((prev) => prev + formatted);
|
|
@@ -180,7 +213,6 @@ function ThreadCard({
|
|
|
180
213
|
className="thread-card-conversation"
|
|
181
214
|
ref={conversationRef}
|
|
182
215
|
onMouseDown={handleConversationMouseDown}
|
|
183
|
-
onMouseUp={handleConversationMouseUp}
|
|
184
216
|
>
|
|
185
217
|
{threadTurns.map((turn) => (
|
|
186
218
|
<ThreadTurnView key={turn.id} turn={turn} />
|
|
@@ -80,4 +80,52 @@ describe('TurnsPanel', () => {
|
|
|
80
80
|
fireEvent.click(screen.getByText('Branch from Here'));
|
|
81
81
|
expect(branchFromTurn).toHaveBeenCalledWith('s1', 't1');
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
it('shows Restore Rewind for rewound non-repoburg sessions', () => {
|
|
85
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1'), makeTurn('t2', 's1')], {
|
|
86
|
+
runtime_type: 'codex',
|
|
87
|
+
rewound_at_turn_id: 't1',
|
|
88
|
+
});
|
|
89
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
90
|
+
render(<TurnsPanel />);
|
|
91
|
+
fireEvent.contextMenu(document.querySelector('.turn-item')!);
|
|
92
|
+
expect(screen.getByText('Restore Rewind')).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('hides Restore Rewind for rewound repoburg sessions (hard delete — no restore)', () => {
|
|
96
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1'), makeTurn('t2', 's1')], {
|
|
97
|
+
runtime_type: 'repoburg',
|
|
98
|
+
rewound_at_turn_id: 't1',
|
|
99
|
+
});
|
|
100
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
101
|
+
render(<TurnsPanel />);
|
|
102
|
+
fireEvent.contextMenu(document.querySelector('.turn-item')!);
|
|
103
|
+
expect(screen.queryByText('Restore Rewind')).not.toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('repoburg rewind confirm says the runtime history is permanently deleted', () => {
|
|
107
|
+
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
|
|
108
|
+
const rewindToTurn = vi.fn();
|
|
109
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1'), makeTurn('t2', 's1')], {
|
|
110
|
+
runtime_type: 'repoburg',
|
|
111
|
+
});
|
|
112
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, rewindToTurn });
|
|
113
|
+
render(<TurnsPanel />);
|
|
114
|
+
fireEvent.contextMenu(document.querySelectorAll('.turn-item')[0]);
|
|
115
|
+
fireEvent.click(screen.getByText('Rewind to Here'));
|
|
116
|
+
expect(confirmSpy).toHaveBeenCalledWith(
|
|
117
|
+
'Rewind to this turn? Runtime history after this point is PERMANENTLY deleted and cannot be restored.',
|
|
118
|
+
);
|
|
119
|
+
expect(rewindToTurn).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('non-repoburg rewind confirm keeps the restore-later copy', () => {
|
|
123
|
+
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
|
|
124
|
+
const detail = makeDetail('s1', [makeTurn('t1', 's1'), makeTurn('t2', 's1')]);
|
|
125
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
126
|
+
render(<TurnsPanel />);
|
|
127
|
+
fireEvent.contextMenu(document.querySelectorAll('.turn-item')[0]);
|
|
128
|
+
fireEvent.click(screen.getByText('Rewind to Here'));
|
|
129
|
+
expect(confirmSpy).toHaveBeenCalledWith('Rewind to this turn? Hidden turns can be restored later.');
|
|
130
|
+
});
|
|
83
131
|
});
|
|
@@ -26,6 +26,11 @@ export default function TurnsPanel() {
|
|
|
26
26
|
const turns = detail?.turns ?? [];
|
|
27
27
|
const isRunning = detail?.is_running === 1;
|
|
28
28
|
const isRewound = !!detail?.rewound_at_turn_id;
|
|
29
|
+
// Repoburg rewind = platform-parity hard delete — no restore, permanent copy.
|
|
30
|
+
const isRepoburg = detail?.runtime_type === 'repoburg';
|
|
31
|
+
const rewindConfirm = isRepoburg
|
|
32
|
+
? 'Rewind to this turn? Runtime history after this point is PERMANENTLY deleted and cannot be restored.'
|
|
33
|
+
: 'Rewind to this turn? Hidden turns can be restored later.';
|
|
29
34
|
|
|
30
35
|
const [ctxMenu, setCtxMenu] = useState<TurnContextMenu | null>(null);
|
|
31
36
|
const ctxRef = useRef<HTMLDivElement>(null);
|
|
@@ -91,7 +96,7 @@ export default function TurnsPanel() {
|
|
|
91
96
|
className={`ctx-menu-item ${ctxMenu.isLast || isRunning || ctxMenu.isSlashCommand ? 'ctx-disabled' : ''}`}
|
|
92
97
|
onClick={() => {
|
|
93
98
|
if (ctxMenu.isLast || isRunning || ctxMenu.isSlashCommand) return;
|
|
94
|
-
if (window.confirm(
|
|
99
|
+
if (window.confirm(rewindConfirm)) {
|
|
95
100
|
rewindToTurn(activeSessionId!, ctxMenu.turnId);
|
|
96
101
|
}
|
|
97
102
|
setCtxMenu(null);
|
|
@@ -99,7 +104,7 @@ export default function TurnsPanel() {
|
|
|
99
104
|
>
|
|
100
105
|
<Rewind size={12} /> Rewind to Here{ctxMenu.isSlashCommand ? ' (slash command)' : ''}
|
|
101
106
|
</div>
|
|
102
|
-
{isRewound && (
|
|
107
|
+
{isRewound && !isRepoburg && (
|
|
103
108
|
<div
|
|
104
109
|
className="ctx-menu-item"
|
|
105
110
|
onClick={() => {
|
|
@@ -94,6 +94,43 @@ export const PROVIDERS: ProviderDef[] = [
|
|
|
94
94
|
},
|
|
95
95
|
],
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
id: 'repoburg',
|
|
99
|
+
label: 'Repoburg',
|
|
100
|
+
available: true,
|
|
101
|
+
// Curated fallback list — live sessions load the full catalog dynamically
|
|
102
|
+
// from GET /api/providers/repoburg/models (repoburg backend model registry).
|
|
103
|
+
models: [
|
|
104
|
+
{
|
|
105
|
+
slug: 'ollama/glm-5.3:cloud',
|
|
106
|
+
displayName: 'GLM-5.3',
|
|
107
|
+
description: 'Ollama Cloud',
|
|
108
|
+
defaultReasoning: 'medium',
|
|
109
|
+
reasoningLevels: [LEVELS.low, LEVELS.medium, LEVELS.high],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
slug: 'ollama/glm-5.3-flash:cloud',
|
|
113
|
+
displayName: 'GLM-5.3 Flash',
|
|
114
|
+
description: 'Ollama Cloud — fast',
|
|
115
|
+
defaultReasoning: 'medium',
|
|
116
|
+
reasoningLevels: [LEVELS.low, LEVELS.medium, LEVELS.high],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
slug: 'ollama/minimax-m3:cloud',
|
|
120
|
+
displayName: 'MiniMax M3',
|
|
121
|
+
description: 'Ollama Cloud',
|
|
122
|
+
defaultReasoning: 'medium',
|
|
123
|
+
reasoningLevels: [LEVELS.low, LEVELS.medium, LEVELS.high],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
slug: 'ollama/deepseek-v4-pro:cloud',
|
|
127
|
+
displayName: 'DeepSeek V4 Pro',
|
|
128
|
+
description: 'Ollama Cloud',
|
|
129
|
+
defaultReasoning: 'medium',
|
|
130
|
+
reasoningLevels: [LEVELS.low, LEVELS.medium, LEVELS.high],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
97
134
|
];
|
|
98
135
|
|
|
99
136
|
/** Get provider by ID. */
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { useResizable } from './useResizable.ts';
|
|
4
|
+
|
|
5
|
+
/** Helper: simulate a native pointer event on window with a clientX.
|
|
6
|
+
* jsdom doesn't implement PointerEvent; MouseEvent carries clientX which is
|
|
7
|
+
* all the hook reads, so we use it as a stand-in. */
|
|
8
|
+
function fireWindowPointer(type: 'pointermove' | 'pointerup', clientX: number): void {
|
|
9
|
+
window.dispatchEvent(new MouseEvent(type, { clientX, bubbles: true }));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Build a minimal React.PointerEvent-like object for onPointerDown. */
|
|
13
|
+
function pointerDown(clientX: number) {
|
|
14
|
+
return {
|
|
15
|
+
clientX,
|
|
16
|
+
preventDefault: vi.fn(),
|
|
17
|
+
stopPropagation: vi.fn(),
|
|
18
|
+
} as unknown as React.PointerEvent;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('useResizable', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
document.body.classList.remove('is-resizing');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('grows left-side width when dragging right (+delta)', () => {
|
|
27
|
+
const getWidth = vi.fn(() => 240);
|
|
28
|
+
const setWidth = vi.fn();
|
|
29
|
+
const { result } = renderHook(() =>
|
|
30
|
+
useResizable({ side: 'left', minWidth: 160, maxWidth: 480, getWidth, setWidth }),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
act(() => {
|
|
34
|
+
result.current.onPointerDown(pointerDown(100));
|
|
35
|
+
});
|
|
36
|
+
expect(result.current.dragging).toBe(true);
|
|
37
|
+
expect(document.body.classList.contains('is-resizing')).toBe(true);
|
|
38
|
+
|
|
39
|
+
act(() => fireWindowPointer('pointermove', 150)); // +50
|
|
40
|
+
expect(setWidth).toHaveBeenLastCalledWith(290);
|
|
41
|
+
|
|
42
|
+
act(() => fireWindowPointer('pointerup', 150));
|
|
43
|
+
expect(result.current.dragging).toBe(false);
|
|
44
|
+
expect(document.body.classList.contains('is-resizing')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('shrinks right-side width when dragging right (-delta)', () => {
|
|
48
|
+
const getWidth = vi.fn(() => 300);
|
|
49
|
+
const setWidth = vi.fn();
|
|
50
|
+
const { result } = renderHook(() =>
|
|
51
|
+
useResizable({ side: 'right', minWidth: 220, maxWidth: 600, getWidth, setWidth }),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
act(() => {
|
|
55
|
+
result.current.onPointerDown(pointerDown(200));
|
|
56
|
+
});
|
|
57
|
+
act(() => fireWindowPointer('pointermove', 260)); // +60
|
|
58
|
+
expect(setWidth).toHaveBeenLastCalledWith(240);
|
|
59
|
+
act(() => fireWindowPointer('pointerup', 260));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('clamps to maxWidth on left side', () => {
|
|
63
|
+
const getWidth = vi.fn(() => 460);
|
|
64
|
+
const setWidth = vi.fn();
|
|
65
|
+
const { result } = renderHook(() =>
|
|
66
|
+
useResizable({ side: 'left', minWidth: 160, maxWidth: 480, getWidth, setWidth }),
|
|
67
|
+
);
|
|
68
|
+
act(() => result.current.onPointerDown(pointerDown(0)));
|
|
69
|
+
act(() => fireWindowPointer('pointermove', 1000));
|
|
70
|
+
expect(setWidth).toHaveBeenLastCalledWith(480);
|
|
71
|
+
act(() => fireWindowPointer('pointerup', 1000));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('clamps to minWidth on right side', () => {
|
|
75
|
+
const getWidth = vi.fn(() => 230);
|
|
76
|
+
const setWidth = vi.fn();
|
|
77
|
+
const { result } = renderHook(() =>
|
|
78
|
+
useResizable({ side: 'right', minWidth: 220, maxWidth: 600, getWidth, setWidth }),
|
|
79
|
+
);
|
|
80
|
+
act(() => result.current.onPointerDown(pointerDown(0)));
|
|
81
|
+
act(() => fireWindowPointer('pointermove', 5000)); // dragging far right shrinks right panel
|
|
82
|
+
expect(setWidth).toHaveBeenLastCalledWith(220);
|
|
83
|
+
act(() => fireWindowPointer('pointerup', 5000));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('stops listening after pointerup', () => {
|
|
87
|
+
const getWidth = vi.fn(() => 240);
|
|
88
|
+
const setWidth = vi.fn();
|
|
89
|
+
const { result } = renderHook(() =>
|
|
90
|
+
useResizable({ side: 'left', minWidth: 160, maxWidth: 480, getWidth, setWidth }),
|
|
91
|
+
);
|
|
92
|
+
act(() => result.current.onPointerDown(pointerDown(0)));
|
|
93
|
+
act(() => fireWindowPointer('pointerup', 0));
|
|
94
|
+
act(() => fireWindowPointer('pointermove', 300));
|
|
95
|
+
expect(setWidth).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ResizeSide = 'left' | 'right';
|
|
4
|
+
|
|
5
|
+
interface UseResizableOptions {
|
|
6
|
+
/** Which edge is being dragged: 'left' grows when dragging right, 'right' grows when dragging left. */
|
|
7
|
+
side: ResizeSide;
|
|
8
|
+
minWidth: number;
|
|
9
|
+
maxWidth: number;
|
|
10
|
+
/** Current width (px) — read live inside the pointer handlers via a ref. */
|
|
11
|
+
getWidth: () => number;
|
|
12
|
+
/** Commit a new width (already clamped by the hook). */
|
|
13
|
+
setWidth: (w: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseResizableResult {
|
|
17
|
+
/** Attach to the drag handle element. */
|
|
18
|
+
onPointerDown: (e: React.PointerEvent) => void;
|
|
19
|
+
/** True while a drag for this handle is in progress. */
|
|
20
|
+
dragging: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Dependency-free horizontal resize handle. Returns a pointerdown handler to
|
|
25
|
+
* attach to a thin handle element; while dragging it updates the store width
|
|
26
|
+
* (clamped to [min, max]) on every pointermove and cleans up on pointerup.
|
|
27
|
+
*
|
|
28
|
+
* Uses window listeners so the drag continues even when the cursor leaves the
|
|
29
|
+
* handle (standard desktop-app behavior).
|
|
30
|
+
*/
|
|
31
|
+
export function useResizable({
|
|
32
|
+
side,
|
|
33
|
+
minWidth,
|
|
34
|
+
maxWidth,
|
|
35
|
+
getWidth,
|
|
36
|
+
setWidth,
|
|
37
|
+
}: UseResizableOptions): UseResizableResult {
|
|
38
|
+
const [dragging, setDragging] = useState(false);
|
|
39
|
+
// Captured at drag start; the listeners close over these refs.
|
|
40
|
+
const startX = useRef(0);
|
|
41
|
+
const startWidth = useRef(0);
|
|
42
|
+
|
|
43
|
+
const onPointerMove = useCallback(
|
|
44
|
+
(e: PointerEvent) => {
|
|
45
|
+
const delta = e.clientX - startX.current;
|
|
46
|
+
const next = side === 'left' ? startWidth.current + delta : startWidth.current - delta;
|
|
47
|
+
const clamped = Math.min(maxWidth, Math.max(minWidth, next));
|
|
48
|
+
setWidth(clamped);
|
|
49
|
+
},
|
|
50
|
+
[side, minWidth, maxWidth, setWidth],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const onPointerUp = useCallback(() => {
|
|
54
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
55
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
56
|
+
document.body.classList.remove('is-resizing');
|
|
57
|
+
setDragging(false);
|
|
58
|
+
}, [onPointerMove]);
|
|
59
|
+
|
|
60
|
+
const onPointerDown = useCallback(
|
|
61
|
+
(e: React.PointerEvent) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
startX.current = e.clientX;
|
|
65
|
+
startWidth.current = getWidth();
|
|
66
|
+
setDragging(true);
|
|
67
|
+
document.body.classList.add('is-resizing');
|
|
68
|
+
window.addEventListener('pointermove', onPointerMove);
|
|
69
|
+
window.addEventListener('pointerup', onPointerUp);
|
|
70
|
+
},
|
|
71
|
+
[getWidth, onPointerMove, onPointerUp],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return { onPointerDown, dragging };
|
|
75
|
+
}
|
|
@@ -438,6 +438,25 @@ describe('onTurnCompleted', () => {
|
|
|
438
438
|
expect(useStore.getState().sessionDetails['s1'].has_unreviewed).toBe(1);
|
|
439
439
|
expect(useStore.getState().sessions[0].has_unreviewed).toBe(1);
|
|
440
440
|
});
|
|
441
|
+
it('stores the response text when provided and not already streamed', () => {
|
|
442
|
+
const turn = makeTurn('t1', 's1', { status: 'running' });
|
|
443
|
+
const detail = makeDetail('s1', [turn], { is_running: 1 });
|
|
444
|
+
resetStore({ sessionDetails: { s1: detail }, sessions: [makeSession('s1', { is_running: 1 })] });
|
|
445
|
+
|
|
446
|
+
useStore.getState().onTurnCompleted('s1', 't1', makeUsage(), true, 'Confirmed: done.');
|
|
447
|
+
|
|
448
|
+
expect(useStore.getState().sessionDetails['s1'].turns[0].agent_response).toBe('Confirmed: done.');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('keeps streamed agent_response over the event response', () => {
|
|
452
|
+
const turn = makeTurn('t1', 's1', { status: 'running', agent_response: 'streamed text' });
|
|
453
|
+
const detail = makeDetail('s1', [turn], { is_running: 1 });
|
|
454
|
+
resetStore({ sessionDetails: { s1: detail }, sessions: [makeSession('s1', { is_running: 1 })] });
|
|
455
|
+
|
|
456
|
+
useStore.getState().onTurnCompleted('s1', 't1', makeUsage(), true, 'event text');
|
|
457
|
+
|
|
458
|
+
expect(useStore.getState().sessionDetails['s1'].turns[0].agent_response).toBe('streamed text');
|
|
459
|
+
});
|
|
441
460
|
|
|
442
461
|
it('does not change has_unreviewed when hasUnreviewed is falsy', () => {
|
|
443
462
|
const turn = makeTurn('t1', 's1', { status: 'running' });
|
|
@@ -1226,6 +1245,41 @@ describe('simple setters', () => {
|
|
|
1226
1245
|
|
|
1227
1246
|
// === New tests: init ===
|
|
1228
1247
|
|
|
1248
|
+
describe('setSidebarWidth / setRightPanelWidth', () => {
|
|
1249
|
+
beforeEach(() => {
|
|
1250
|
+
vi.clearAllMocks();
|
|
1251
|
+
resetStore();
|
|
1252
|
+
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {});
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it('clamps sidebar width to [SIDEBAR_MIN, SIDEBAR_MAX] and persists', () => {
|
|
1256
|
+
const setItem = vi.mocked(Storage.prototype.setItem);
|
|
1257
|
+
useStore.getState().setSidebarWidth(900);
|
|
1258
|
+
expect(useStore.getState().sidebarWidth).toBe(480);
|
|
1259
|
+
expect(setItem).toHaveBeenCalledWith('agentpane:sidebar-width', '480');
|
|
1260
|
+
|
|
1261
|
+
useStore.getState().setSidebarWidth(50);
|
|
1262
|
+
expect(useStore.getState().sidebarWidth).toBe(160);
|
|
1263
|
+
expect(setItem).toHaveBeenCalledWith('agentpane:sidebar-width', '160');
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
it('clamps right panel width to [RIGHT_PANEL_MIN, RIGHT_PANEL_MAX] and persists', () => {
|
|
1267
|
+
const setItem = vi.mocked(Storage.prototype.setItem);
|
|
1268
|
+
useStore.getState().setRightPanelWidth(9999);
|
|
1269
|
+
expect(useStore.getState().rightPanelWidth).toBe(600);
|
|
1270
|
+
expect(setItem).toHaveBeenCalledWith('agentpane:right-panel-width', '600');
|
|
1271
|
+
|
|
1272
|
+
useStore.getState().setRightPanelWidth(10);
|
|
1273
|
+
expect(useStore.getState().rightPanelWidth).toBe(220);
|
|
1274
|
+
expect(setItem).toHaveBeenCalledWith('agentpane:right-panel-width', '220');
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
it('rounds fractional widths', () => {
|
|
1278
|
+
useStore.getState().setSidebarWidth(240.7);
|
|
1279
|
+
expect(useStore.getState().sidebarWidth).toBe(241);
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1229
1283
|
describe('init', () => {
|
|
1230
1284
|
beforeEach(() => {
|
|
1231
1285
|
vi.clearAllMocks();
|
|
@@ -3,6 +3,30 @@ import type { LogEntry, RightTab, ChangesScope, ModelInfo } from '../types/index
|
|
|
3
3
|
import type { StoreState, Toast } from './store.ts';
|
|
4
4
|
import { fetchModels } from '../api/models.ts';
|
|
5
5
|
|
|
6
|
+
/** Default + clamp bounds for resizable side panels (px). */
|
|
7
|
+
export const SIDEBAR_MIN = 160;
|
|
8
|
+
export const SIDEBAR_MAX = 480;
|
|
9
|
+
export const SIDEBAR_DEFAULT = 240;
|
|
10
|
+
export const RIGHT_PANEL_MIN = 220;
|
|
11
|
+
export const RIGHT_PANEL_MAX = 600;
|
|
12
|
+
export const RIGHT_PANEL_DEFAULT = 300;
|
|
13
|
+
|
|
14
|
+
const SIDEBAR_KEY = 'agentpane:sidebar-width';
|
|
15
|
+
const RIGHT_PANEL_KEY = 'agentpane:right-panel-width';
|
|
16
|
+
|
|
17
|
+
/** Read a stored px width, falling back to `fallback` on missing/invalid data. */
|
|
18
|
+
function readWidth(key: string, min: number, max: number, fallback: number): number {
|
|
19
|
+
try {
|
|
20
|
+
const raw = localStorage.getItem(key);
|
|
21
|
+
if (raw == null) return fallback;
|
|
22
|
+
const n = Number(raw);
|
|
23
|
+
if (!Number.isFinite(n)) return fallback;
|
|
24
|
+
return Math.min(max, Math.max(min, n));
|
|
25
|
+
} catch {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
export type UiSlice = {
|
|
7
31
|
activeRightTab: RightTab;
|
|
8
32
|
changesScope: ChangesScope;
|
|
@@ -17,6 +41,8 @@ export type UiSlice = {
|
|
|
17
41
|
models: ModelInfo[];
|
|
18
42
|
modelsLoading: boolean;
|
|
19
43
|
showModelSelector: boolean;
|
|
44
|
+
sidebarWidth: number;
|
|
45
|
+
rightPanelWidth: number;
|
|
20
46
|
|
|
21
47
|
setActiveTurnIndex: (idx: number) => void;
|
|
22
48
|
switchRightTab: (tab: RightTab) => void;
|
|
@@ -25,6 +51,8 @@ export type UiSlice = {
|
|
|
25
51
|
setShowCommandPalette: (show: boolean) => void;
|
|
26
52
|
setShowModelSelector: (show: boolean) => void;
|
|
27
53
|
setSelectedChangePath: (path: string | null) => void;
|
|
54
|
+
setSidebarWidth: (w: number) => void;
|
|
55
|
+
setRightPanelWidth: (w: number) => void;
|
|
28
56
|
addToast: (type: Toast['type'], message: string, undoAction?: () => void, undoLabel?: string) => void;
|
|
29
57
|
removeToast: (id: string) => void;
|
|
30
58
|
addLogEntry: (level: LogEntry['level'], message: string) => void;
|
|
@@ -45,6 +73,8 @@ export const createUiSlice: StateCreator<StoreState, [], [], UiSlice> = (set, ge
|
|
|
45
73
|
models: [],
|
|
46
74
|
modelsLoading: false,
|
|
47
75
|
showModelSelector: false,
|
|
76
|
+
sidebarWidth: readWidth(SIDEBAR_KEY, SIDEBAR_MIN, SIDEBAR_MAX, SIDEBAR_DEFAULT),
|
|
77
|
+
rightPanelWidth: readWidth(RIGHT_PANEL_KEY, RIGHT_PANEL_MIN, RIGHT_PANEL_MAX, RIGHT_PANEL_DEFAULT),
|
|
48
78
|
|
|
49
79
|
setActiveTurnIndex: (idx) => set({ activeTurnIndex: idx }),
|
|
50
80
|
switchRightTab: (tab) => set({ activeRightTab: tab }),
|
|
@@ -54,6 +84,26 @@ export const createUiSlice: StateCreator<StoreState, [], [], UiSlice> = (set, ge
|
|
|
54
84
|
setShowModelSelector: (show) => set({ showModelSelector: show }),
|
|
55
85
|
setSelectedChangePath: (path) => set({ selectedChangePath: path }),
|
|
56
86
|
|
|
87
|
+
setSidebarWidth: (w) => {
|
|
88
|
+
const clamped = Math.min(SIDEBAR_MAX, Math.max(SIDEBAR_MIN, Math.round(w)));
|
|
89
|
+
set({ sidebarWidth: clamped });
|
|
90
|
+
try {
|
|
91
|
+
localStorage.setItem(SIDEBAR_KEY, String(clamped));
|
|
92
|
+
} catch {
|
|
93
|
+
/* ignore quota / disabled storage */
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
setRightPanelWidth: (w) => {
|
|
98
|
+
const clamped = Math.min(RIGHT_PANEL_MAX, Math.max(RIGHT_PANEL_MIN, Math.round(w)));
|
|
99
|
+
set({ rightPanelWidth: clamped });
|
|
100
|
+
try {
|
|
101
|
+
localStorage.setItem(RIGHT_PANEL_KEY, String(clamped));
|
|
102
|
+
} catch {
|
|
103
|
+
/* ignore quota / disabled storage */
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
57
107
|
addToast: (type, message, undoAction, undoLabel) => {
|
|
58
108
|
const id = Math.random().toString(36).slice(2, 9);
|
|
59
109
|
set((state) => ({ toasts: [...state.toasts, { id, type, message, undoAction, undoLabel }] }));
|
|
@@ -16,7 +16,7 @@ export type WsSlice = {
|
|
|
16
16
|
onItemUpdated: (sessionId: string, turnId: string, item: Item) => void;
|
|
17
17
|
onItemCompleted: (sessionId: string, turnId: string, item: Item) => void;
|
|
18
18
|
onItemDelta: (sessionId: string, turnId: string, itemId: string, delta: string) => void;
|
|
19
|
-
onTurnCompleted: (sessionId: string, turnId: string, usage?: TokenUsage | null, hasUnreviewed?: boolean) => void;
|
|
19
|
+
onTurnCompleted: (sessionId: string, turnId: string, usage?: TokenUsage | null, hasUnreviewed?: boolean, response?: string) => void;
|
|
20
20
|
onTokenUsageUpdated: (sessionId: string, turnId: string, usage: TokenUsage) => void;
|
|
21
21
|
onContextUsageUpdated: (sessionId: string, ctx: ContextUsage) => void;
|
|
22
22
|
onTurnFailed: (sessionId: string, turnId: string, error: string) => void;
|
|
@@ -141,7 +141,7 @@ export const createWsSlice: StateCreator<StoreState, [], [], WsSlice> = (set, ge
|
|
|
141
141
|
});
|
|
142
142
|
},
|
|
143
143
|
|
|
144
|
-
onTurnCompleted: (sessionId, turnId, usage, hasUnreviewed) => {
|
|
144
|
+
onTurnCompleted: (sessionId, turnId, usage, hasUnreviewed, response) => {
|
|
145
145
|
const inTok = usage?.input_tokens ?? 0;
|
|
146
146
|
const outTok = usage?.output_tokens ?? 0;
|
|
147
147
|
get().addLogEntry('INFO', `turn:completed — ${inTok} in / ${outTok} out`);
|
|
@@ -160,7 +160,14 @@ export const createWsSlice: StateCreator<StoreState, [], [], WsSlice> = (set, ge
|
|
|
160
160
|
has_unreviewed: hasUnreviewed ? 1 : detail.has_unreviewed,
|
|
161
161
|
turns: detail.turns.map((t) =>
|
|
162
162
|
t.id === turnId
|
|
163
|
-
? {
|
|
163
|
+
? {
|
|
164
|
+
...t,
|
|
165
|
+
status: 'completed',
|
|
166
|
+
token_usage: usage ?? t.token_usage ?? null,
|
|
167
|
+
// Final text for turns that produce no agent_message item
|
|
168
|
+
// (repoburg final-action rounds) — keep any streamed text.
|
|
169
|
+
...(response && !t.agent_response ? { agent_response: response } : {}),
|
|
170
|
+
}
|
|
164
171
|
: t,
|
|
165
172
|
),
|
|
166
173
|
},
|