ompchamber 1.0.0 → 1.2.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.
- package/dist/client/index.html +1 -1
- package/dist/client/static/css/index.b938f4a693.css +2 -0
- package/dist/client/static/js/77.8022a800b5.js +139 -0
- package/dist/client/static/js/async/{1k.5974df3b87.js → 1k.a411fe5b43.js} +2 -2
- package/dist/client/static/js/async/{3y.71f2ccf3b6.js → 3y.3186ecdd63.js} +5 -5
- package/dist/client/static/js/async/5b.d88304bc13.js +1 -0
- package/dist/client/static/js/async/66.86544ad741.js +93 -0
- package/dist/client/static/js/async/7g.db82aca7ba.js +1 -0
- package/dist/client/static/js/async/7l.fadbbc0583.js +1 -0
- package/dist/client/static/js/async/{dc.856fda6560.js → ar.ad5c8822a8.js} +2 -2
- package/dist/client/static/js/async/cx.9c8268d542.js +226 -0
- package/dist/client/static/js/async/e.6443e739a2.js +79 -0
- package/dist/client/static/js/async/f7.46971e9d6d.js +2 -0
- package/dist/client/static/js/async/g.ee41bfed7f.js +2 -0
- package/dist/client/static/js/async/h1.d7091cef78.js +1 -0
- package/dist/client/static/js/async/hs.e7c59d8090.js +41 -0
- package/dist/client/static/js/async/jl.4911fa3383.js +1 -0
- package/dist/client/static/js/async/{k.7466330d1a.js → k.d16cd09ae7.js} +2 -2
- package/dist/client/static/js/async/{m.c038c71b9f.js → m.2e1ea3db9e.js} +2 -2
- package/dist/client/static/js/async/m3.d2b05f9604.js +2 -0
- package/dist/client/static/js/async/mt.853be2b5aa.js +1 -0
- package/dist/client/static/js/async/{oc.9c060c9779.js → oc.eb22093f05.js} +1 -1
- package/dist/client/static/js/async/{og.634fba99ac.js → og.53f068804c.js} +1 -1
- package/dist/client/static/js/async/{5b.0d6ae50fe8.js → q.b453193538.js} +10 -10
- package/dist/client/static/js/async/q5.88b50ffa95.js +286 -0
- package/dist/client/static/js/async/q8.7f1b48e9cd.js +21 -0
- package/dist/client/static/js/async/{sy.d16be843a7.js → s.aad2ecebc6.js} +1 -1
- package/dist/client/static/js/async/{tr.e878658140.js → tr.2a0d2b7600.js} +1 -1
- package/dist/client/static/js/async/vt.20406a5604.js +1 -0
- package/dist/client/static/js/async/x1.fe8af17016.js +90 -0
- package/dist/client/static/js/async/yg.5a94e1fc8a.js +149 -0
- package/dist/client/static/js/async/yx.af2936c368.js +10 -0
- package/dist/client/static/js/index.57b39ffa7f.js +34 -0
- package/package.json +1 -1
- package/src/client/components/common/ComposerPicker.tsx +16 -4
- package/src/client/components/common/ComposerTextarea.tsx +1 -0
- package/src/client/components/common/file-icon/index.tsx +4 -1
- package/src/client/components/common/image-viewer/index.tsx +224 -0
- package/src/client/components/layout/RightActivityBar.tsx +1 -1
- package/src/client/components/layout/desktop-layout/ResizeHandle.tsx +2 -2
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +13 -2
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +107 -56
- package/src/client/components/layout/desktop-layout/index.tsx +25 -96
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +79 -118
- package/src/client/components/layout/desktop-layout/resizer/separator-drag.ts +140 -0
- package/src/client/components/layout/session-sidebar/Header.tsx +1 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +26 -12
- package/src/client/components/layout/session-sidebar/SubagentList.tsx +6 -4
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +57 -43
- package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +31 -24
- package/src/client/components/settings/Modal.tsx +1 -0
- package/src/client/components/settings/categories/ChatSettings.tsx +6 -2
- package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +99 -0
- package/src/client/components/workspace/btw-panel/AskCard.tsx +155 -0
- package/src/client/components/workspace/btw-panel/Composer.tsx +181 -0
- package/src/client/components/workspace/btw-panel/Messages.tsx +100 -0
- package/src/client/components/workspace/btw-panel/TopicMenu.tsx +106 -0
- package/src/client/components/workspace/btw-panel/attachments.ts +80 -0
- package/src/client/components/workspace/btw-panel/index.tsx +83 -0
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +39 -20
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +3 -0
- package/src/client/components/workspace/chat-timeline/MinimapShortcuts.tsx +49 -12
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +3 -1
- package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +10 -6
- package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/chat-input/DropOverlay.tsx +17 -0
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +67 -43
- package/src/client/components/workspace/chat-timeline/index.tsx +14 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Goal.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +25 -7
- package/src/client/components/workspace/editor/Toolbar.tsx +32 -23
- package/src/client/components/workspace/editor/index.tsx +45 -36
- package/src/client/components/workspace/model-dropdown/groups.ts +35 -2
- package/src/client/components/workspace/model-dropdown/index.tsx +48 -7
- package/src/client/components/workspace/model-dropdown/use-catalog.ts +28 -4
- package/src/client/hooks/chat/btw/index.ts +180 -0
- package/src/client/hooks/chat/btw/intercept.ts +43 -0
- package/src/client/hooks/chat/btw/mode.ts +179 -0
- package/src/client/hooks/chat/composer/attachments.ts +198 -0
- package/src/client/hooks/chat/composer/drop-entries.ts +204 -0
- package/src/client/hooks/chat/composer/drop-payload.test.ts +97 -0
- package/src/client/hooks/chat/composer/drop-payload.ts +130 -0
- package/src/client/hooks/chat/composer/drop-references.ts +106 -0
- package/src/client/hooks/chat/composer/file-drop.test.ts +194 -0
- package/src/client/hooks/chat/composer/file-drop.ts +226 -0
- package/src/client/hooks/chat/composer/file-reads.test.ts +211 -0
- package/src/client/hooks/chat/composer/file-reads.ts +238 -0
- package/src/client/hooks/chat/composer/index.ts +35 -9
- package/src/client/hooks/chat/composer/pipeline.ts +181 -0
- package/src/client/hooks/chat/omp/index.ts +3 -3
- package/src/client/hooks/chat/omp/prompt-send.ts +5 -5
- package/src/client/hooks/chat/timeline/actions.ts +40 -4
- package/src/client/hooks/chat/timeline/deferred-model.test.ts +219 -0
- package/src/client/hooks/chat/timeline/deferred-model.ts +92 -0
- package/src/client/hooks/chat/timeline/index.ts +29 -9
- package/src/client/hooks/chat/timeline/prompt-text.ts +6 -1
- package/src/client/hooks/chat/timeline/send.ts +45 -70
- package/src/client/hooks/chat/timeline/session-load.ts +2 -1
- package/src/client/hooks/chat/timeline/session-pagination.ts +161 -14
- package/src/client/hooks/chat/timeline/user-turns.ts +149 -0
- package/src/client/hooks/editor/file-actions.ts +49 -0
- package/src/client/hooks/editor/use-file-editor.ts +38 -27
- package/src/client/hooks/editor/use-file-transfer.ts +78 -0
- package/src/client/hooks/workspace/available-width.ts +33 -0
- package/src/client/hooks/workspace/panel-widths.ts +34 -37
- package/src/client/tailwind.css +22 -0
- package/src/server/db.server.ts +21 -287
- package/src/server/lib/btw/frames.test.ts +68 -0
- package/src/server/lib/btw/frames.ts +56 -0
- package/src/server/lib/btw/prompt.ts +28 -0
- package/src/server/lib/btw/registry.server.ts +141 -0
- package/src/server/lib/btw/runtime.server.ts +324 -0
- package/src/server/lib/btw/schema.server.ts +44 -0
- package/src/server/lib/btw/service.server.ts +171 -0
- package/src/server/lib/btw/session-copy.server.ts +213 -0
- package/src/server/lib/btw/session-copy.test.ts +166 -0
- package/src/server/lib/btw/store.server.ts +240 -0
- package/src/server/lib/db/schema.ts +110 -0
- package/src/server/lib/db/seed.ts +160 -0
- package/src/server/lib/db/workspace-sync.ts +70 -0
- package/src/server/lib/fs/root.ts +37 -0
- package/src/server/lib/models/preferences.server.ts +110 -0
- package/src/server/lib/models/registry.server.ts +206 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +207 -0
- package/src/server/lib/omp/rpc/manager.ts +27 -123
- package/src/server/lib/omp/rpc/utility.ts +7 -1
- package/src/server/lib/omp/session/auto-title.server.test.ts +137 -0
- package/src/server/lib/omp/session/auto-title.server.ts +152 -0
- package/src/server/lib/omp/session/blobs.server.test.ts +100 -0
- package/src/server/lib/omp/session/blobs.server.ts +76 -0
- package/src/server/lib/omp/session/file-scan.ts +174 -0
- package/src/server/lib/omp/session/files.test.ts +147 -0
- package/src/server/lib/omp/session/files.ts +52 -35
- package/src/server/lib/omp/session/locator.ts +2 -2
- package/src/server/lib/omp/session/merge-stored.test.ts +118 -0
- package/src/server/lib/omp/session/merge-stored.ts +171 -0
- package/src/server/lib/omp/session/messages.ts +34 -0
- package/src/server/lib/omp/session/reader.ts +2 -2
- package/src/server/lib/omp/session/subagent-presence.ts +9 -6
- package/src/server/lib/queue/delivery.server.test.ts +260 -0
- package/src/server/lib/queue/delivery.server.ts +63 -11
- package/src/server/plugins/ssr.ts +29 -2
- package/src/server/routes/btw/events.ts +70 -0
- package/src/server/routes/btw/index.ts +8 -0
- package/src/server/routes/btw/topics.ts +87 -0
- package/src/server/routes/btw/ws.ts +136 -0
- package/src/server/routes/chat/index.ts +2 -0
- package/src/server/routes/chat/session.ts +1 -115
- package/src/server/routes/chat/turns.ts +87 -0
- package/src/server/routes/fs/index.ts +4 -1
- package/src/server/routes/fs/raw.ts +85 -0
- package/src/server/routes/fs/read.ts +58 -3
- package/src/server/routes/index.ts +7 -1
- package/src/server/routes/models/root.ts +88 -215
- package/src/server/routes/settings/commands.ts +59 -18
- package/src/shared/lib/chat/attachments.test.ts +208 -0
- package/src/shared/lib/chat/attachments.ts +200 -15
- package/src/shared/lib/chat/btw/client.ts +94 -0
- package/src/shared/lib/chat/composer/client.test.ts +48 -0
- package/src/shared/lib/chat/composer/client.ts +23 -9
- package/src/shared/lib/chat/composer/commands.ts +290 -0
- package/src/shared/lib/chat/composer/composer.test.ts +195 -0
- package/src/shared/lib/chat/composer/filter.ts +21 -7
- package/src/shared/lib/chat/composer/trigger.ts +154 -21
- package/src/shared/lib/chat/omp/agent-events.ts +33 -80
- package/src/shared/lib/chat/omp/socket.ts +11 -6
- package/src/shared/lib/chat/omp/sse.ts +11 -6
- package/src/shared/lib/chat/omp/tool-results.ts +102 -0
- package/src/shared/lib/chat/omp/transport.ts +19 -3
- package/src/shared/lib/chat/text-file.ts +148 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -32
- package/src/shared/lib/chat/timeline/stream-coalescer.ts +66 -0
- package/src/shared/lib/chat/timeline/turns.ts +7 -0
- package/src/shared/lib/fs/file-kind.ts +50 -0
- package/src/shared/lib/fs/paths.ts +19 -1
- package/src/shared/lib/markdown/math.test.ts +20 -3
- package/src/shared/lib/models/preferences.test.ts +131 -0
- package/src/shared/lib/models/preferences.ts +88 -0
- package/src/shared/lib/models/provider-models.ts +8 -1
- package/src/shared/lib/omp/session/inlined-attachments.test.ts +82 -0
- package/src/shared/lib/omp/session/mapper.ts +24 -4
- package/src/shared/lib/omp/session/messages-map.ts +16 -3
- package/src/shared/lib/omp/session/messages-parse.ts +2 -2
- package/src/shared/lib/omp/session/parse-message-blocks.ts +85 -5
- package/src/shared/lib/omp/subagent/label.test.ts +94 -0
- package/src/shared/lib/omp/subagent/label.ts +84 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +113 -18
- package/src/shared/lib/workspace/panel-widths.ts +161 -30
- package/src/shared/lib/workspace/right-panels.ts +50 -11
- package/src/shared/lib/workspace/session-state/store.ts +3 -0
- package/src/shared/types/btw.ts +65 -0
- package/src/shared/types/chat.ts +77 -66
- package/src/shared/types/index.ts +2 -0
- package/src/shared/types/models.ts +101 -0
- package/src/shared/types/omp/agent.ts +14 -1
- package/src/shared/types/omp/session.ts +4 -1
- package/src/shared/types/omp/subagent.ts +4 -0
- package/src/shared/types/settings/command.ts +13 -0
- package/src/shared/types/settings/state.ts +5 -0
- package/dist/client/static/css/index.e880517fcd.css +0 -2
- package/dist/client/static/js/async/5bh.8b2363424f.js +0 -1
- package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
- package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
- package/dist/client/static/js/async/7l.7a6a377ecc.js +0 -1
- package/dist/client/static/js/async/cx.ca5feec570.js +0 -226
- package/dist/client/static/js/async/e.93214d622b.js +0 -79
- package/dist/client/static/js/async/g.a06ca3b3e9.js +0 -2
- package/dist/client/static/js/async/h1.e8d5aa8e80.js +0 -1
- package/dist/client/static/js/async/hs.6d480d0ded.js +0 -41
- package/dist/client/static/js/async/jl.874d352278.js +0 -1
- package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
- package/dist/client/static/js/async/nam.b5663e919f.js +0 -10
- package/dist/client/static/js/async/q.23af2a358f.js +0 -21
- package/dist/client/static/js/async/q5.e683f7c26e.js +0 -286
- package/dist/client/static/js/async/s.222ee974f6.js +0 -2
- package/dist/client/static/js/async/uf.46ad6209fa.js +0 -2
- package/dist/client/static/js/async/vt.42416b707d.js +0 -1
- package/dist/client/static/js/async/x1.da8687159b.js +0 -90
- package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
- package/dist/client/static/js/index.0de15c5e17.js +0 -33
- package/dist/client/static/js/jp.50817896f3.js +0 -139
- /package/dist/client/static/js/{jp.50817896f3.js.LICENSE.txt → 77.8022a800b5.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{1k.5974df3b87.js.LICENSE.txt → 1k.a411fe5b43.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.71f2ccf3b6.js.LICENSE.txt → 3y.3186ecdd63.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{dc.856fda6560.js.LICENSE.txt → ar.ad5c8822a8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.c038c71b9f.js.LICENSE.txt → f7.46971e9d6d.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{5b.0d6ae50fe8.js.LICENSE.txt → g.ee41bfed7f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.23af2a358f.js.LICENSE.txt → m.2e1ea3db9e.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{s.222ee974f6.js.LICENSE.txt → m3.d2b05f9604.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.a06ca3b3e9.js.LICENSE.txt → q.b453193538.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{uf.46ad6209fa.js.LICENSE.txt → q8.7f1b48e9cd.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{nam.b5663e919f.js.LICENSE.txt → yx.af2936c368.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.0de15c5e17.js.LICENSE.txt → index.57b39ffa7f.js.LICENSE.txt} +0 -0
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
import { useState } from 'preact/hooks';
|
|
2
|
+
import type { UserTurnRef } from '@/shared/types';
|
|
3
|
+
import { formatMessageStamp } from '@/shared/lib/format/time';
|
|
2
4
|
|
|
3
5
|
interface MinimapShortcutsProps {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
/** Every user turn of the session, oldest first — not just the mounted
|
|
7
|
+
* window, so the rail lists the whole conversation while the timeline holds
|
|
8
|
+
* one page of it. */
|
|
9
|
+
turns: UserTurnRef[];
|
|
10
|
+
/** A jump is paging history to reach the clicked turn. */
|
|
11
|
+
jumping: boolean;
|
|
12
|
+
onJumpTurn: (turn: UserTurnRef) => void;
|
|
6
13
|
}
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
/** Row pitch the rail keeps while it fits: a 1px line with 3px of breathing
|
|
16
|
+
* room on each side — the spacing the hover wave was drawn around. */
|
|
17
|
+
const ROW_PITCH_MAX = '7px';
|
|
18
|
+
/** Rows never collapse below this, so a tick stays clickable. */
|
|
19
|
+
const ROW_PITCH_MIN = '2px';
|
|
20
|
+
/** Target ceiling for the rail as a share of the viewport: a session long
|
|
21
|
+
* enough to overflow it tightens its rows rather than growing past the
|
|
22
|
+
* timeline. Past the minimum pitch the class-level `max-h` takes over, so a
|
|
23
|
+
* session with hundreds of turns still keeps every tick on screen. */
|
|
24
|
+
const RAIL_TARGET_HEIGHT = '60vh';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Jump rail: one tick per user turn of the FULL history. A turn outside the
|
|
28
|
+
* mounted window is still clickable — the click pages older windows in until
|
|
29
|
+
* that row exists, then centers it.
|
|
30
|
+
*/
|
|
31
|
+
export function MinimapShortcuts({ turns, jumping, onJumpTurn }: MinimapShortcutsProps) {
|
|
9
32
|
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
10
|
-
if (
|
|
33
|
+
if (turns.length === 0) return null;
|
|
34
|
+
|
|
35
|
+
// Rows are laid out in normal flow (never absolutely positioned), so their
|
|
36
|
+
// hit boxes cannot overlap and each tick keeps its own hover.
|
|
37
|
+
const rowPitch = `clamp(${ROW_PITCH_MIN}, calc(${RAIL_TARGET_HEIGHT} / ${turns.length}), ${ROW_PITCH_MAX})`;
|
|
11
38
|
|
|
12
39
|
const lineWidth = (idx: number): string => {
|
|
13
40
|
if (hoveredIdx === null) return 'w-3';
|
|
@@ -19,25 +46,35 @@ export function MinimapShortcuts({ userMessages, onScrollTo }: MinimapShortcutsP
|
|
|
19
46
|
|
|
20
47
|
return (
|
|
21
48
|
<div
|
|
22
|
-
className=
|
|
49
|
+
className={`absolute right-0.5 top-1/2 -translate-y-1/2 z-20 flex flex-col items-center py-4 w-6 max-h-[min(60vh,100%)] ${jumping ? 'animate-pulse' : ''}`}
|
|
50
|
+
role="navigation"
|
|
51
|
+
aria-label={`Jump to your message (${turns.length} turns)`}
|
|
23
52
|
onMouseLeave={() => setHoveredIdx(null)}
|
|
24
53
|
>
|
|
25
|
-
{
|
|
54
|
+
{turns.map((turn, idx) => {
|
|
26
55
|
const isHovered = hoveredIdx === idx;
|
|
27
56
|
return (
|
|
28
57
|
<div
|
|
29
|
-
key={`minimap-${
|
|
30
|
-
className="relative flex items-center justify-center w-full
|
|
58
|
+
key={`minimap-${turn.id}`}
|
|
59
|
+
className="relative flex min-h-0 items-center justify-center w-full cursor-pointer"
|
|
60
|
+
style={{ height: rowPitch }}
|
|
31
61
|
onMouseEnter={() => setHoveredIdx(idx)}
|
|
32
|
-
onClick={() =>
|
|
62
|
+
onClick={() => onJumpTurn(turn)}
|
|
33
63
|
>
|
|
34
64
|
<button
|
|
65
|
+
type="button"
|
|
35
66
|
className={`block h-px rounded-full transition-all duration-200 ease-out ${lineWidth(idx)} ${isHovered ? 'bg-ink h-[2px]' : 'bg-ink/25'}`}
|
|
36
67
|
aria-label={`Jump to your message #${idx + 1}`}
|
|
37
68
|
/>
|
|
38
|
-
<div
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
<div
|
|
70
|
+
className={`absolute right-full mr-3 top-1/2 -translate-y-1/2 w-64 p-3 bg-ink text-canvas text-xs rounded-md shadow-lg pointer-events-none transition-opacity duration-200 z-30 ${isHovered ? 'opacity-100' : 'opacity-0'}`}
|
|
71
|
+
>
|
|
72
|
+
<div className="flex items-center justify-between gap-2 pb-1.5 mb-1.5 border-b border-canvas/20 font-mono text-[10px] uppercase tracking-wider text-canvas/60">
|
|
73
|
+
<span>#{idx + 1} of {turns.length}</span>
|
|
74
|
+
{formatMessageStamp(turn) && <span>{formatMessageStamp(turn)}</span>}
|
|
75
|
+
</div>
|
|
76
|
+
<div className="line-clamp-3 whitespace-pre-wrap leading-relaxed">{turn.preview.trim() || '(no text)'}</div>
|
|
77
|
+
<div className="absolute top-1/2 -right-1 -translate-y-1/2 w-2 h-2 bg-ink rotate-45" />
|
|
41
78
|
</div>
|
|
42
79
|
</div>
|
|
43
80
|
);
|
|
@@ -8,6 +8,7 @@ import { RunFooter } from '@/client/components/workspace/chat-timeline/RunFooter
|
|
|
8
8
|
import { useSubagentTranscript } from '@/client/hooks/chat/subagent';
|
|
9
9
|
import { useModelNames } from '@/client/hooks/models/use-model-names';
|
|
10
10
|
import { isNoticeRow } from '@/shared/lib/chat/notice-row';
|
|
11
|
+
import { subagentHeaderTitle } from '@/shared/lib/omp/subagent/label';
|
|
11
12
|
import { previousNonNoticeIndex, resolveRunFooters, streamingRowIndex } from '@/shared/lib/chat/timeline/run-footer';
|
|
12
13
|
import { formatCompactTokens } from '@/shared/lib/format/number';
|
|
13
14
|
|
|
@@ -62,6 +63,7 @@ export function SubagentView({ sessionId, subagent, onBack, provider, providerNa
|
|
|
62
63
|
|
|
63
64
|
const progress = status?.progress ?? subagent.progress;
|
|
64
65
|
const modelName = progress?.resolvedModel ?? subagent.agent;
|
|
66
|
+
const headerTitle = subagentHeaderTitle(status ?? subagent);
|
|
65
67
|
const compactTokens = formatCompactTokens(progress?.tokens);
|
|
66
68
|
const meta = compactTokens ? `${compactTokens} tok` : '';
|
|
67
69
|
const rawTask = status?.task ?? status?.assignment ?? status?.description
|
|
@@ -85,7 +87,7 @@ export function SubagentView({ sessionId, subagent, onBack, provider, providerNa
|
|
|
85
87
|
<div className="min-w-0 flex-1">
|
|
86
88
|
<div className="flex items-center gap-2 min-w-0">
|
|
87
89
|
<span className="font-mono text-[12px] font-semibold text-ink truncate">
|
|
88
|
-
{
|
|
90
|
+
{headerTitle}
|
|
89
91
|
</span>
|
|
90
92
|
<span className="shrink-0 rounded bg-ink/5 px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-wider text-ink/50">
|
|
91
93
|
{STATUS_LABEL[effectiveStatus]}
|
|
@@ -3,12 +3,15 @@ import { ArrowDown } from 'lucide-preact';
|
|
|
3
3
|
import { MessageList } from '@/client/components/workspace/chat-timeline/MessageList';
|
|
4
4
|
import { MinimapShortcuts } from '@/client/components/workspace/chat-timeline/MinimapShortcuts';
|
|
5
5
|
import { LoadingOlderIndicator } from '@/client/components/workspace/chat-timeline/SessionSkeleton';
|
|
6
|
-
import type { ChatMessageData } from '@/shared/types';
|
|
6
|
+
import type { ChatMessageData, UserTurnRef } from '@/shared/types';
|
|
7
7
|
|
|
8
8
|
interface TimelineBodyProps {
|
|
9
9
|
isMobile: boolean;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** Every user turn of the session (full history), for the jump rail. */
|
|
11
|
+
userTurns: UserTurnRef[];
|
|
12
|
+
/** A jump is paging history to reach the clicked turn. */
|
|
13
|
+
jumpingTurn: boolean;
|
|
14
|
+
onJumpTurn: (turn: UserTurnRef) => void;
|
|
12
15
|
scrollRef: RefObject<HTMLDivElement>;
|
|
13
16
|
contentRef: (node: HTMLDivElement | null) => void;
|
|
14
17
|
handleScroll: () => void;
|
|
@@ -38,8 +41,9 @@ interface TimelineBodyProps {
|
|
|
38
41
|
*/
|
|
39
42
|
export function TimelineBody({
|
|
40
43
|
isMobile,
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
userTurns,
|
|
45
|
+
jumpingTurn,
|
|
46
|
+
onJumpTurn,
|
|
43
47
|
scrollRef,
|
|
44
48
|
contentRef,
|
|
45
49
|
handleScroll,
|
|
@@ -66,7 +70,7 @@ export function TimelineBody({
|
|
|
66
70
|
{/* Minimap Shortcuts — desktop only: the rail needs side room a
|
|
67
71
|
phone does not have. */}
|
|
68
72
|
{!isMobile && (
|
|
69
|
-
<MinimapShortcuts
|
|
73
|
+
<MinimapShortcuts turns={userTurns} jumping={jumpingTurn} onJumpTurn={onJumpTurn} />
|
|
70
74
|
)}
|
|
71
75
|
|
|
72
76
|
{/* Timeline Body */}
|
|
@@ -2,6 +2,7 @@ import { useRef } from 'preact/hooks';
|
|
|
2
2
|
import type { ChangeEvent } from 'preact/compat';
|
|
3
3
|
import { File as FileIcon, Paperclip, X } from 'lucide-preact';
|
|
4
4
|
import type { Attachment } from '@/shared/types';
|
|
5
|
+
import { attachmentName } from '@/shared/lib/chat/attachments';
|
|
5
6
|
|
|
6
7
|
interface AttachmentToolbarProps {
|
|
7
8
|
attachments: Attachment[];
|
|
@@ -39,7 +40,7 @@ export function AttachmentToolbar({ attachments, onFilesSelected, onRemove }: At
|
|
|
39
40
|
<FileIcon size={12} />
|
|
40
41
|
</div>
|
|
41
42
|
)}
|
|
42
|
-
<span className="truncate max-w-[120px] font-mono text-[10px] text-ink/80">{att
|
|
43
|
+
<span className="truncate max-w-[120px] font-mono text-[10px] text-ink/80">{attachmentName(att)}</span>
|
|
43
44
|
<button
|
|
44
45
|
onClick={() => onRemove(att.id)}
|
|
45
46
|
className="absolute right-0.5 top-1/2 -translate-y-1/2 p-0.5 text-ink/40 hover:text-error hover:bg-error/10 rounded transition-colors"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { UploadCloud } from 'lucide-preact';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Covers the composer while a file drag hovers it. `pointer-events-none` is
|
|
5
|
+
* load-bearing: the overlay sits above the card, so without it the drop would
|
|
6
|
+
* land on the overlay and never reach the card's own handler.
|
|
7
|
+
*/
|
|
8
|
+
export function DropOverlay({ visible }: { visible: boolean }) {
|
|
9
|
+
if (!visible) return null;
|
|
10
|
+
return (
|
|
11
|
+
<div className="pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-1 rounded-md border-2 border-dashed border-ink/40 bg-canvas/90 text-ink">
|
|
12
|
+
<UploadCloud size={18} className="text-ink/60" />
|
|
13
|
+
<span className="text-[11px] font-semibold">Drop files to attach</span>
|
|
14
|
+
<span className="text-[10px] text-ink/50">Folders are expanded into their files</span>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import type { ClipboardEvent, SetStateAction } from 'preact/compat';
|
|
3
|
+
import { AlertTriangle, X } from 'lucide-preact';
|
|
3
4
|
import type { AIModelOption, Attachment, ModelEntry } from '@/shared/types';
|
|
4
5
|
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
5
6
|
import { ComposerToolbar } from '@/client/components/workspace/chat-timeline/chat-input/Toolbar';
|
|
@@ -8,6 +9,10 @@ import { AttachmentToolbar } from '@/client/components/workspace/chat-timeline/c
|
|
|
8
9
|
import { selectableThinkingLevels } from '@/shared/lib/models/thinking-levels';
|
|
9
10
|
import { fetchModelsData, subscribeModelsUpdated } from '@/shared/lib/models/client';
|
|
10
11
|
import { resolveThinkingLevel, selectionFor } from '@/client/components/workspace/chat-timeline/chat-input/selection';
|
|
12
|
+
import { NO_PENDING_PICK } from '@/client/hooks/chat/timeline/deferred-model';
|
|
13
|
+
import { primeFileReads } from '@/client/hooks/chat/composer/file-reads';
|
|
14
|
+
import { useComposerPipeline } from '@/client/hooks/chat/composer/pipeline';
|
|
15
|
+
import { DropOverlay } from '@/client/components/workspace/chat-timeline/chat-input/DropOverlay';
|
|
11
16
|
|
|
12
17
|
export function ChatInput({
|
|
13
18
|
value,
|
|
@@ -31,6 +36,12 @@ export function ChatInput({
|
|
|
31
36
|
/** Written on every model/thinking pick so the send path can snapshot the
|
|
32
37
|
* selection into queued items without lifting ChatInput state. */
|
|
33
38
|
composerModelRef,
|
|
39
|
+
/** A pick the user made while a turn was streaming, still waiting for the
|
|
40
|
+
* next prompt. While set, the session's own model must NOT be adopted over
|
|
41
|
+
* it — the composer shows what the next prompt will actually run. Omitted
|
|
42
|
+
* by composers that never stream (the New Chat modal), where there is
|
|
43
|
+
* nothing to defer and adoption must behave as before. */
|
|
44
|
+
deferredComposerPickRef = NO_PENDING_PICK,
|
|
34
45
|
}: {
|
|
35
46
|
value: string;
|
|
36
47
|
onChange: (v: string) => void;
|
|
@@ -58,6 +69,7 @@ export function ChatInput({
|
|
|
58
69
|
*/
|
|
59
70
|
variant?: 'desktop' | 'mobile';
|
|
60
71
|
composerModelRef: { current: { provider: string; modelId: string; thinkingLevel: string } | null };
|
|
72
|
+
deferredComposerPickRef?: { current: { provider?: string; modelId?: string; thinkingLevel?: string } | null };
|
|
61
73
|
}) {
|
|
62
74
|
const [internalAttachments, setInternalAttachments] = useState<Attachment[]>([]);
|
|
63
75
|
|
|
@@ -102,12 +114,18 @@ export function ChatInput({
|
|
|
102
114
|
// this effect re-runs right after a spawn (sessionModel changes while the
|
|
103
115
|
// session's own thinking_level_change entry is not readable yet), and any
|
|
104
116
|
// concrete fallback here claims a level the run never used.
|
|
117
|
+
//
|
|
118
|
+
// A pick still waiting for the next prompt wins: the session's model is what
|
|
119
|
+
// the RUNNING turn uses, and adopting it would erase the user's choice from
|
|
120
|
+
// the composer even though it is what the next prompt will run with.
|
|
105
121
|
useEffect(() => {
|
|
106
122
|
if (!sessionModel?.provider || !sessionModel.modelId) return;
|
|
123
|
+
if (deferredComposerPickRef.current) return;
|
|
107
124
|
let active = true;
|
|
108
125
|
fetchModelsData()
|
|
109
126
|
.then((data) => {
|
|
110
127
|
if (!active || !sessionModel) return;
|
|
128
|
+
if (deferredComposerPickRef.current) return;
|
|
111
129
|
const match = (data.modelList || []).find(
|
|
112
130
|
(m) => m.id === sessionModel.modelId && m.provider === sessionModel.provider
|
|
113
131
|
);
|
|
@@ -119,18 +137,21 @@ export function ChatInput({
|
|
|
119
137
|
return () => {
|
|
120
138
|
active = false;
|
|
121
139
|
};
|
|
122
|
-
}, [sessionModel?.provider, sessionModel?.modelId]);
|
|
140
|
+
}, [sessionModel?.provider, sessionModel?.modelId, deferredComposerPickRef]);
|
|
123
141
|
|
|
124
142
|
// Adopt the active session's last-used thinking level. Runs after the model
|
|
125
143
|
// sync effect above so it overrides the catalog default for this session.
|
|
144
|
+
// Same rule as the model: a pick waiting for the next prompt is not
|
|
145
|
+
// overridden by the level the running turn recorded.
|
|
126
146
|
useEffect(() => {
|
|
127
147
|
if (!sessionThinkingLevel) return;
|
|
148
|
+
if (deferredComposerPickRef.current?.thinkingLevel) return;
|
|
128
149
|
setSelectedModel(prev => {
|
|
129
150
|
if (!prev) return prev;
|
|
130
151
|
const level = resolveThinkingLevel(prev.thinkingLevels, sessionThinkingLevel, prev.thinkingLevel);
|
|
131
152
|
return level === prev.thinkingLevel ? prev : { ...prev, thinkingLevel: level };
|
|
132
153
|
});
|
|
133
|
-
}, [sessionThinkingLevel]);
|
|
154
|
+
}, [sessionThinkingLevel, deferredComposerPickRef]);
|
|
134
155
|
|
|
135
156
|
// Sync selected model from server and event listener
|
|
136
157
|
useEffect(() => {
|
|
@@ -139,6 +160,9 @@ export function ChatInput({
|
|
|
139
160
|
try {
|
|
140
161
|
const data = await fetchModelsData();
|
|
141
162
|
if (!active) return;
|
|
163
|
+
// A pick waiting for the next prompt owns the composer's selection:
|
|
164
|
+
// every branch below would replace it with a session/catalog default.
|
|
165
|
+
if (deferredComposerPickRef.current) return;
|
|
142
166
|
if (Array.isArray(data.modelList) && data.modelList.length > 0) {
|
|
143
167
|
// 1. The active session's last-used model is authoritative; even when
|
|
144
168
|
// it is missing from the catalog, never replace it with the default.
|
|
@@ -183,7 +207,7 @@ export function ChatInput({
|
|
|
183
207
|
active = false;
|
|
184
208
|
unsubscribe();
|
|
185
209
|
};
|
|
186
|
-
}, []);
|
|
210
|
+
}, [deferredComposerPickRef]);
|
|
187
211
|
|
|
188
212
|
// Thinking Dropdown State - Reactive with selectedModel
|
|
189
213
|
const thinkingLevels = useMemo(() => {
|
|
@@ -210,61 +234,61 @@ export function ChatInput({
|
|
|
210
234
|
if (clipboard.files.length > 0) {
|
|
211
235
|
e.preventDefault();
|
|
212
236
|
const files = Array.from(clipboard.files);
|
|
213
|
-
|
|
237
|
+
// Same rule as a drop: the clipboard's file permission is released when
|
|
238
|
+
// this handler returns, so the reads are started here, in its tick.
|
|
239
|
+
acceptFiles(files, false, [], primeFileReads(files));
|
|
214
240
|
}
|
|
215
241
|
};
|
|
216
242
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
reader.onload = () => {
|
|
229
|
-
const result = reader.result as string;
|
|
230
|
-
const base64 = result.split(',')[1];
|
|
231
|
-
if (!base64) return;
|
|
232
|
-
setAttachments(prev => prev.map(a => (a.id === att.id ? { ...a, dataBase64: base64 } : a)));
|
|
233
|
-
};
|
|
234
|
-
reader.readAsDataURL(att.file);
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const removeAttachment = (id: string) => {
|
|
239
|
-
setAttachments(prev => {
|
|
240
|
-
const att = prev.find(p => p.id === id);
|
|
241
|
-
if (att && att.preview) URL.revokeObjectURL(att.preview);
|
|
242
|
-
return prev.filter(p => p.id !== id);
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const handleSendClick = (options?: { steering?: boolean }) => {
|
|
247
|
-
if (!value.trim() && attachments.length === 0) return;
|
|
248
|
-
onSend(attachments, options);
|
|
249
|
-
setAttachments([]);
|
|
250
|
-
};
|
|
243
|
+
// Attach-and-send pipeline: drop handling, the inline notice, and the send
|
|
244
|
+
// guard all live in the hook, because a drop registers files before this
|
|
245
|
+
// component re-renders and the send decision cannot read a stale prop.
|
|
246
|
+
const { notice, dismissNotice, isDragging, dropProps, acceptFiles, removeAttachment, submit } = useComposerPipeline({
|
|
247
|
+
attachments,
|
|
248
|
+
setAttachments,
|
|
249
|
+
value,
|
|
250
|
+
disabled,
|
|
251
|
+
rootPath,
|
|
252
|
+
onSend,
|
|
253
|
+
});
|
|
251
254
|
|
|
252
255
|
const isMobile = variant === 'mobile';
|
|
253
256
|
|
|
254
257
|
return (
|
|
255
|
-
<div
|
|
256
|
-
|
|
258
|
+
<div
|
|
259
|
+
className={`relative border rounded-md bg-paper transition-colors flex flex-col shadow-sm ${
|
|
260
|
+
isDragging ? 'border-ink/40' : 'border-ink/20 focus-within:border-ink'
|
|
261
|
+
} ${className}`}
|
|
262
|
+
{...dropProps}
|
|
263
|
+
>
|
|
264
|
+
<DropOverlay visible={isDragging} />
|
|
265
|
+
|
|
257
266
|
<AttachmentToolbar
|
|
258
267
|
attachments={attachments}
|
|
259
|
-
onFilesSelected={
|
|
268
|
+
onFilesSelected={(files) => acceptFiles(files)}
|
|
260
269
|
onRemove={removeAttachment}
|
|
261
270
|
/>
|
|
262
271
|
|
|
272
|
+
{notice && (
|
|
273
|
+
<div className="flex items-start gap-1.5 border-b border-error/20 bg-error/5 px-3 py-1.5 text-[11px] text-error">
|
|
274
|
+
<AlertTriangle size={11} className="mt-0.5 shrink-0" />
|
|
275
|
+
<span className="min-w-0 flex-1 break-words">{notice}</span>
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
onClick={() => dismissNotice()}
|
|
279
|
+
className="shrink-0 text-error/60 hover:text-error transition-colors"
|
|
280
|
+
title="Dismiss"
|
|
281
|
+
>
|
|
282
|
+
<X size={11} />
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
|
|
263
287
|
{/* Textarea */}
|
|
264
288
|
<ComposerTextarea
|
|
265
289
|
value={value}
|
|
266
290
|
onChange={onChange}
|
|
267
|
-
onSend={
|
|
291
|
+
onSend={submit}
|
|
268
292
|
disabled={disabled}
|
|
269
293
|
appSettings={appSettings}
|
|
270
294
|
rootPath={rootPath}
|
|
@@ -290,7 +314,7 @@ export function ChatInput({
|
|
|
290
314
|
onThinkingLevelChange={onThinkingLevelChange}
|
|
291
315
|
isGenerating={isGenerating}
|
|
292
316
|
onStop={onStop}
|
|
293
|
-
onSend={() =>
|
|
317
|
+
onSend={() => submit()}
|
|
294
318
|
sendDisabled={disabled || (!value.trim() && attachments.length === 0)}
|
|
295
319
|
accessMode={accessMode}
|
|
296
320
|
onSelectAccess={onAccessModeChange}
|
|
@@ -3,6 +3,7 @@ import { useSearchParams } from '@/client/lib/router/search-params';
|
|
|
3
3
|
import { ExtensionDialog } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
|
|
4
4
|
import { AskFramesContext, splitAskFrames, type AskFramesHandle } from '@/client/hooks/chat/timeline/ask-frames';
|
|
5
5
|
import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
|
|
6
|
+
import type { UserTurnRef } from '@/shared/types/chat';
|
|
6
7
|
import type { ExtensionDialogResponse } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
|
|
7
8
|
import { EmptyWorkspacePrompt } from '@/client/components/workspace/chat-timeline/EmptyWorkspacePrompt';
|
|
8
9
|
import { SessionSkeleton } from '@/client/components/workspace/chat-timeline/SessionSkeleton';
|
|
@@ -47,6 +48,9 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
47
48
|
hasMore,
|
|
48
49
|
loadingOlder, loadOlderError, sessionLoading,
|
|
49
50
|
loadOlder,
|
|
51
|
+
userTurns,
|
|
52
|
+
jumpingTurn,
|
|
53
|
+
jumpToUserTurn,
|
|
50
54
|
generatingVerb,
|
|
51
55
|
messageQueue,
|
|
52
56
|
removeMessage,
|
|
@@ -75,6 +79,7 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
75
79
|
accessMode,
|
|
76
80
|
handleAccessModeChange,
|
|
77
81
|
composerModelRef,
|
|
82
|
+
deferredComposerPickRef,
|
|
78
83
|
extensionDialogs,
|
|
79
84
|
resolveExtensionDialog,
|
|
80
85
|
respondToExtensionUi,
|
|
@@ -126,18 +131,14 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
126
131
|
|
|
127
132
|
useSessionTitle(sessionId, sessionData?.title, onSessionTitle);
|
|
128
133
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
133
|
-
}
|
|
134
|
-
}, []);
|
|
134
|
+
const handleJumpTurn = useCallback((turn: UserTurnRef) => {
|
|
135
|
+
void jumpToUserTurn(turn.id, turn.index);
|
|
136
|
+
}, [jumpToUserTurn]);
|
|
135
137
|
|
|
136
138
|
const handleNewChat = useCallback((content: string) => setNewChatInitialContent(content), []);
|
|
137
139
|
|
|
138
140
|
const modelNames = useModelNames();
|
|
139
141
|
const providerNames = useProviderNames();
|
|
140
|
-
const userMessages = useMemo(() => localMessages.filter(m => m.role === 'user'), [localMessages]);
|
|
141
142
|
const sessionProvider = typeof sessionData?.model === 'object' ? sessionData.model.provider : undefined;
|
|
142
143
|
const sessionModelName = typeof sessionData?.model === 'object'
|
|
143
144
|
? (modelNames[sessionData.model.modelId] ?? sessionData.model.modelId)
|
|
@@ -180,6 +181,7 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
180
181
|
accessMode={accessMode}
|
|
181
182
|
onAccessModeChange={handleAccessModeChange}
|
|
182
183
|
composerModelRef={composerModelRef}
|
|
184
|
+
deferredComposerPickRef={deferredComposerPickRef}
|
|
183
185
|
sessionModel={typeof sessionData?.model === 'object' ? sessionData.model : null}
|
|
184
186
|
sessionThinkingLevel={sessionData?.thinkingLevel}
|
|
185
187
|
generatingVerb={generatingVerb}
|
|
@@ -208,8 +210,9 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
208
210
|
) : (
|
|
209
211
|
<TimelineBody
|
|
210
212
|
isMobile={isMobile}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
userTurns={userTurns}
|
|
214
|
+
jumpingTurn={jumpingTurn}
|
|
215
|
+
onJumpTurn={handleJumpTurn}
|
|
213
216
|
scrollRef={scrollRef}
|
|
214
217
|
contentRef={contentRef}
|
|
215
218
|
handleScroll={handleScroll}
|
|
@@ -238,6 +241,7 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
238
241
|
{!activeSubagent && (
|
|
239
242
|
<ComposerDock
|
|
240
243
|
isMobile={isMobile}
|
|
244
|
+
sessionId={sessionId}
|
|
241
245
|
isGenerating={isGenerating}
|
|
242
246
|
modelName={sessionModelName}
|
|
243
247
|
generatingVerb={generatingVerb}
|
|
@@ -263,6 +267,7 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
263
267
|
accessMode={accessMode}
|
|
264
268
|
onAccessModeChange={handleAccessModeChange}
|
|
265
269
|
composerModelRef={composerModelRef}
|
|
270
|
+
deferredComposerPickRef={deferredComposerPickRef}
|
|
266
271
|
sessionModel={typeof sessionData?.model === 'object' ? sessionData.model : null}
|
|
267
272
|
sessionThinkingLevel={sessionData?.thinkingLevel}
|
|
268
273
|
variant={variant}
|
|
@@ -124,9 +124,9 @@ export function Goal({ tool }: { tool: ToolCallData }) {
|
|
|
124
124
|
{payload.summary && (
|
|
125
125
|
<div>
|
|
126
126
|
<span className="text-[10px] font-mono text-ink/45 uppercase tracking-wider">Summary</span>
|
|
127
|
-
<
|
|
128
|
-
{payload.summary}
|
|
129
|
-
</
|
|
127
|
+
<div className="mt-0.5 rounded border border-ink/6 bg-ink/[0.03] p-2">
|
|
128
|
+
<MarkdownRenderer content={payload.summary} className="font-medium" />
|
|
129
|
+
</div>
|
|
130
130
|
</div>
|
|
131
131
|
)}
|
|
132
132
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
2
|
-
import { FileCode, FileText, Folder, FolderOpen, Info, Loader2 } from 'lucide-preact';
|
|
2
|
+
import { FileCode, FileImage, FileText, Folder, FolderOpen, Info, Loader2 } from 'lucide-preact';
|
|
3
3
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
4
4
|
import { getLanguageFromPath, highlightCode } from '@/shared/lib/code/syntax-highlight';
|
|
5
5
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
6
6
|
import { parseDirListing, parseNumberedCode } from '@/shared/lib/code/parser';
|
|
7
|
+
import { getImageMimeType } from '@/shared/lib/fs/file-kind';
|
|
8
|
+
import { buildFsRawUrl } from '@/shared/lib/fs/paths';
|
|
7
9
|
import type { ToolCallData } from '@/shared/types/chat';
|
|
8
10
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
9
11
|
import { extractLineMeta } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/read-line-meta';
|
|
@@ -23,9 +25,13 @@ export function Read({ tool, targetFilePath, output }: ReadPanelProps) {
|
|
|
23
25
|
const inputPath = getToolInputPath(tool?.input);
|
|
24
26
|
const filePath = targetFilePath || inputPath || tool?.target || '';
|
|
25
27
|
const cleanFetchPath = filePath.split('?')[0].split('#')[0].replace(/:\d+(?:-\d+)?$/, '');
|
|
28
|
+
// An image read has no text to show: the panel paints the picture instead of
|
|
29
|
+
// the code surface, and `/api/fs/raw` is what carries its bytes.
|
|
30
|
+
const isImage = getImageMimeType(cleanFetchPath) !== null;
|
|
31
|
+
const imageUrl = isImage ? buildFsRawUrl({ path: cleanFetchPath }) : null;
|
|
26
32
|
|
|
27
33
|
useEffect(() => {
|
|
28
|
-
if (cleanFetchPath && !output && lazyContent === null && !loadingFile) {
|
|
34
|
+
if (cleanFetchPath && !isImage && !output && lazyContent === null && !loadingFile) {
|
|
29
35
|
setLoadingFile(true);
|
|
30
36
|
fetch(`/api/fs/read?path=${encodeURIComponent(cleanFetchPath)}`)
|
|
31
37
|
.then((res) => res.json())
|
|
@@ -39,7 +45,7 @@ export function Read({ tool, targetFilePath, output }: ReadPanelProps) {
|
|
|
39
45
|
setLoadingFile(false);
|
|
40
46
|
});
|
|
41
47
|
}
|
|
42
|
-
}, [cleanFetchPath, output, lazyContent, loadingFile]);
|
|
48
|
+
}, [cleanFetchPath, output, lazyContent, loadingFile, isImage]);
|
|
43
49
|
|
|
44
50
|
const details = (tool?.details ?? {}) as Record<string, any>;
|
|
45
51
|
const rawContent =
|
|
@@ -107,6 +113,7 @@ export function Read({ tool, targetFilePath, output }: ReadPanelProps) {
|
|
|
107
113
|
|
|
108
114
|
const isDir = dirInfo.isDirectory;
|
|
109
115
|
const lang = getLanguageFromPath(filePath);
|
|
116
|
+
const kindLabel = isDir ? 'Directory' : isImage ? 'Image' : lang;
|
|
110
117
|
|
|
111
118
|
const visibleEntries = dirInfo.entries.slice(-MAX_OUTPUT_LINES);
|
|
112
119
|
const skippedEntries = dirInfo.entries.length - visibleEntries.length;
|
|
@@ -126,17 +133,17 @@ export function Read({ tool, targetFilePath, output }: ReadPanelProps) {
|
|
|
126
133
|
<div className="flex items-center justify-between rounded-lg border border-ink/8 bg-paper px-3 py-2">
|
|
127
134
|
<div className="flex min-w-0 items-center gap-2">
|
|
128
135
|
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-ink/5 text-ink/70">
|
|
129
|
-
{isDir ? <FolderOpen size={12} /> : <FileCode size={12} />}
|
|
136
|
+
{isDir ? <FolderOpen size={12} /> : isImage ? <FileImage size={12} /> : <FileCode size={12} />}
|
|
130
137
|
</span>
|
|
131
138
|
<span className="truncate font-mono text-[11px] font-medium text-ink">
|
|
132
139
|
{filePath || (isDir ? 'Directory Listing' : 'File Content')}
|
|
133
140
|
</span>
|
|
134
141
|
<span className="rounded bg-ink/5 px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-wider text-ink/50">
|
|
135
|
-
{
|
|
142
|
+
{kindLabel}
|
|
136
143
|
</span>
|
|
137
144
|
</div>
|
|
138
145
|
|
|
139
|
-
{rawContent && (
|
|
146
|
+
{rawContent && !isImage && (
|
|
140
147
|
<CopyButton
|
|
141
148
|
text={parsedCode.hasLineNumbers ? parsedCode.cleanCode : (rawContent || '')}
|
|
142
149
|
className="flex shrink-0 cursor-pointer items-center gap-1 rounded px-1.5 py-0.5 text-[10.5px] text-ink/50 transition-colors hover:bg-ink/5 hover:text-ink"
|
|
@@ -146,7 +153,18 @@ export function Read({ tool, targetFilePath, output }: ReadPanelProps) {
|
|
|
146
153
|
)}
|
|
147
154
|
</div>
|
|
148
155
|
|
|
149
|
-
{
|
|
156
|
+
{isImage && imageUrl ? (
|
|
157
|
+
/* An image read has no code to show: paint the bytes. */
|
|
158
|
+
<div className="overflow-hidden rounded-lg border border-ink/8 bg-paper">
|
|
159
|
+
<div className="flex max-h-80 items-center justify-center overflow-auto bg-canvas/60 p-3">
|
|
160
|
+
<img
|
|
161
|
+
src={imageUrl}
|
|
162
|
+
alt={cleanFetchPath}
|
|
163
|
+
className="max-h-72 max-w-full object-contain"
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
) : loadingFile ? (
|
|
150
168
|
<div className="flex items-center gap-2 rounded-lg border border-ink/8 bg-paper px-3 py-3 font-mono text-[11px] text-ink/50">
|
|
151
169
|
<Loader2 size={12} className="animate-spin" />
|
|
152
170
|
<span>Loading file content...</span>
|