@bibobonking/opensol 0.0.0-stage → 3.0.2
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/.github/workflows/build.yml +25 -0
- package/README.md +74 -2
- package/dist/assets/index-BmN7pBji.css +1 -0
- package/dist/assets/index-DgNDjkOz.js +301 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +55 -0
- package/dist/preview-host.html +33 -0
- package/index.html +54 -0
- package/package.json +43 -4
- package/postcss.config.js +6 -0
- package/public/favicon.svg +15 -0
- package/public/preview-host.html +33 -0
- package/server/README.md +75 -0
- package/server/daemon.mjs +1082 -0
- package/server/lib/checkpoints.mjs +124 -0
- package/server/lib/checkpoints.test.mjs +110 -0
- package/server/lib/deploy.mjs +436 -0
- package/server/lib/http.mjs +237 -0
- package/server/lib/plugins.mjs +321 -0
- package/server/lib/plugins.test.mjs +140 -0
- package/server/lib/providers.mjs +594 -0
- package/server/lib/store.mjs +491 -0
- package/server/lib/tools.mjs +990 -0
- package/server/opensol.mjs +202 -0
- package/server/package.json +42 -0
- package/shared/plugin-builder.test.mjs +43 -0
- package/shared/plugin-package.d.mts +34 -0
- package/shared/plugin-package.mjs +197 -0
- package/shared/plugin-package.test.mjs +94 -0
- package/src/App.tsx +152 -0
- package/src/agent/adapters/anthropic.ts +233 -0
- package/src/agent/adapters/gemini.ts +225 -0
- package/src/agent/adapters/openai-responses.ts +215 -0
- package/src/agent/adapters/openai.ts +200 -0
- package/src/agent/adapters/shared.ts +96 -0
- package/src/agent/approval.ts +20 -0
- package/src/agent/compact.ts +234 -0
- package/src/agent/index.ts +36 -0
- package/src/agent/model.ts +20 -0
- package/src/agent/prompt.ts +107 -0
- package/src/agent/queue.test.mjs +111 -0
- package/src/agent/queue.ts +84 -0
- package/src/agent/subagent.ts +109 -0
- package/src/agent/tools.ts +541 -0
- package/src/components/ChatView.tsx +403 -0
- package/src/components/CommandPalette.tsx +416 -0
- package/src/components/Composer.tsx +678 -0
- package/src/components/ContextBar.tsx +58 -0
- package/src/components/DeployModal.tsx +512 -0
- package/src/components/ErrorBoundary.tsx +75 -0
- package/src/components/HomeDashboard.tsx +189 -0
- package/src/components/Logo.tsx +41 -0
- package/src/components/Markdown.tsx +232 -0
- package/src/components/MessageItem.tsx +655 -0
- package/src/components/ModelPicker.tsx +487 -0
- package/src/components/PixleSol.tsx +243 -0
- package/src/components/PluginBuilder.tsx +285 -0
- package/src/components/PluginManager.tsx +341 -0
- package/src/components/PreviewPane.tsx +166 -0
- package/src/components/SettingsModal.tsx +2052 -0
- package/src/components/Sidebar.tsx +504 -0
- package/src/components/Splash.tsx +104 -0
- package/src/components/ToolCallCard.tsx +177 -0
- package/src/components/TopBar.tsx +182 -0
- package/src/components/TrajectoryView.tsx +178 -0
- package/src/components/WorkspacePanel.tsx +710 -0
- package/src/components/ui.tsx +311 -0
- package/src/core/kernel.ts +45 -0
- package/src/core/plugins/tools/fs.ts +219 -0
- package/src/core/plugins/tools/git.ts +83 -0
- package/src/core/plugins/tools/grep.ts +98 -0
- package/src/core/plugins/tools/shell.ts +88 -0
- package/src/core/plugins/tools/subagent.ts +36 -0
- package/src/core/plugins/tools/web.ts +97 -0
- package/src/core/services/agent-loop.ts +417 -0
- package/src/core/services/approval.ts +66 -0
- package/src/core/services/compaction.ts +45 -0
- package/src/core/services/llm.ts +32 -0
- package/src/core/services/sessions.ts +47 -0
- package/src/core/services/subagents.ts +15 -0
- package/src/core/services/system-prompt.ts +177 -0
- package/src/core/services/tools.ts +135 -0
- package/src/core/services/ui.ts +109 -0
- package/src/core/types.ts +139 -0
- package/src/data/providers.ts +125 -0
- package/src/data/skills.ts +68 -0
- package/src/index.css +776 -0
- package/src/lib/boot.ts +94 -0
- package/src/lib/context.ts +75 -0
- package/src/lib/conversation.test.mjs +64 -0
- package/src/lib/conversation.ts +59 -0
- package/src/lib/daemon.ts +475 -0
- package/src/lib/hooks.ts +33 -0
- package/src/lib/permissions.ts +60 -0
- package/src/lib/pixlesol.test.mjs +51 -0
- package/src/lib/pixlesol.ts +55 -0
- package/src/lib/plugin-folder.test.mjs +145 -0
- package/src/lib/plugin-folder.ts +96 -0
- package/src/lib/reasoning.ts +87 -0
- package/src/lib/sse.ts +75 -0
- package/src/lib/themes.test.mjs +72 -0
- package/src/lib/themes.ts +52 -0
- package/src/lib/usage.test.mjs +72 -0
- package/src/lib/usage.ts +155 -0
- package/src/lib/utils.ts +73 -0
- package/src/main.tsx +16 -0
- package/src/plugins/api.test.mjs +77 -0
- package/src/plugins/api.ts +113 -0
- package/src/plugins/bridge.d.mts +1 -0
- package/src/plugins/bridge.mjs +26 -0
- package/src/plugins/bridge.test.mjs +17 -0
- package/src/plugins/index.ts +52 -0
- package/src/plugins/preview.test.mjs +18 -0
- package/src/plugins/preview.ts +34 -0
- package/src/plugins/runtime.ts +194 -0
- package/src/plugins/types.ts +40 -0
- package/src/store/chats.ts +278 -0
- package/src/store/models.ts +120 -0
- package/src/store/settings.ts +445 -0
- package/src/store/ui.ts +132 -0
- package/src/store/workspace.ts +299 -0
- package/src/types.ts +337 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +83 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +19 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { ArrowRight } from 'lucide-react'
|
|
3
|
+
import type { ToolMessage, ToolName } from '../types'
|
|
4
|
+
import { toolLabel, toolSummary } from '../agent/tools'
|
|
5
|
+
import { resolveApproval } from '../agent/approval'
|
|
6
|
+
import { useUi } from '../store/ui'
|
|
7
|
+
import { cn } from '../lib/utils'
|
|
8
|
+
|
|
9
|
+
function statusMeta(status: ToolMessage['status']) {
|
|
10
|
+
switch (status) {
|
|
11
|
+
case 'awaiting':
|
|
12
|
+
return { label: 'Needs approval', className: 'text-warning', dot: 'bg-warning' }
|
|
13
|
+
case 'running':
|
|
14
|
+
return { label: 'Running', className: 'text-accent', dot: 'bg-accent' }
|
|
15
|
+
case 'done':
|
|
16
|
+
return { label: 'Done', className: 'text-success', dot: 'bg-success' }
|
|
17
|
+
case 'denied':
|
|
18
|
+
return { label: 'Denied', className: 'text-muted', dot: 'bg-faint' }
|
|
19
|
+
default:
|
|
20
|
+
return { label: 'Failed', className: 'text-danger', dot: 'bg-danger' }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function durationLabel(ms?: number) {
|
|
25
|
+
if (ms === undefined) return ''
|
|
26
|
+
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function approvalCopy(name: ToolName) {
|
|
30
|
+
switch (name) {
|
|
31
|
+
case 'shell':
|
|
32
|
+
return 'OpenSol wants to run this command in the open workspace.'
|
|
33
|
+
case 'write_file':
|
|
34
|
+
return 'OpenSol wants to create or overwrite this file on disk.'
|
|
35
|
+
case 'edit_file':
|
|
36
|
+
case 'rewrite_file':
|
|
37
|
+
return 'OpenSol wants to change this file on disk.'
|
|
38
|
+
case 'git':
|
|
39
|
+
return 'OpenSol wants to change the Git state of this repository.'
|
|
40
|
+
case 'subagent':
|
|
41
|
+
return 'OpenSol wants to start a read-only subagent to investigate.'
|
|
42
|
+
case 'web_search':
|
|
43
|
+
return 'OpenSol wants to search the web.'
|
|
44
|
+
case 'read_url':
|
|
45
|
+
return 'OpenSol wants to read this public page.'
|
|
46
|
+
default:
|
|
47
|
+
return 'OpenSol wants to use this tool.'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ToolCallCard({ message, compact = false }: { message: ToolMessage; compact?: boolean }) {
|
|
52
|
+
const [open, setOpen] = useState((!compact && message.status === 'awaiting') || message.status === 'error')
|
|
53
|
+
const [copied, setCopied] = useState(false)
|
|
54
|
+
const notify = useUi((state) => state.notify)
|
|
55
|
+
const status = statusMeta(message.status)
|
|
56
|
+
const summary = toolSummary(message.call.name, message.call.args)
|
|
57
|
+
const args = message.call.args
|
|
58
|
+
const command = typeof args.command === 'string' ? args.command : ''
|
|
59
|
+
const content =
|
|
60
|
+
typeof args.content === 'string'
|
|
61
|
+
? args.content
|
|
62
|
+
: typeof args.newText === 'string'
|
|
63
|
+
? args.newText
|
|
64
|
+
: typeof args.oldText === 'string'
|
|
65
|
+
? args.oldText
|
|
66
|
+
: ''
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (message.status === 'awaiting' || message.status === 'error') setOpen(true)
|
|
70
|
+
}, [message.status])
|
|
71
|
+
|
|
72
|
+
const copyOutput = async () => {
|
|
73
|
+
try {
|
|
74
|
+
await navigator.clipboard.writeText(message.result ?? '')
|
|
75
|
+
setCopied(true)
|
|
76
|
+
window.setTimeout(() => setCopied(false), 1600)
|
|
77
|
+
} catch {
|
|
78
|
+
notify('Clipboard access was blocked by the browser.', 'error')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const trailing =
|
|
83
|
+
message.status === 'done' && message.durationMs !== undefined
|
|
84
|
+
? durationLabel(message.durationMs)
|
|
85
|
+
: message.status === 'done'
|
|
86
|
+
? ''
|
|
87
|
+
: status.label.toLowerCase()
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="text-[12.5px]">
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => setOpen((value) => !value)}
|
|
94
|
+
className="group/row flex w-full min-w-0 items-baseline gap-1.5 py-[3px] text-left"
|
|
95
|
+
aria-expanded={open}
|
|
96
|
+
>
|
|
97
|
+
<ArrowRight
|
|
98
|
+
size={12}
|
|
99
|
+
strokeWidth={2}
|
|
100
|
+
className={cn(
|
|
101
|
+
'relative top-[1px] shrink-0 self-center text-faint transition-transform duration-200 ease-out group-hover/row:text-ink',
|
|
102
|
+
open && 'rotate-90',
|
|
103
|
+
)}
|
|
104
|
+
/>
|
|
105
|
+
<span className="shrink-0 font-medium text-ink">{toolLabel(message.call.name)}</span>
|
|
106
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[11.5px] text-muted">{summary || '—'}</span>
|
|
107
|
+
{trailing && (
|
|
108
|
+
<span className={cn('shrink-0 text-[11px]', message.status === 'running' ? 'text-shimmer' : message.status === 'done' ? 'text-faint' : status.className)}>
|
|
109
|
+
{trailing}
|
|
110
|
+
</span>
|
|
111
|
+
)}
|
|
112
|
+
</button>
|
|
113
|
+
|
|
114
|
+
{message.status === 'awaiting' && (
|
|
115
|
+
<div className="animate-fade py-1 pl-[18px]">
|
|
116
|
+
<p className="text-[12px] leading-relaxed text-warning">{approvalCopy(message.call.name)}</p>
|
|
117
|
+
<p className="mt-1 flex items-center gap-4 text-[12px] font-medium">
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
onClick={() => resolveApproval(message.call.id, true)}
|
|
121
|
+
className="text-ink underline decoration-ink/30 underline-offset-[3px] transition-colors hover:decoration-ink"
|
|
122
|
+
>
|
|
123
|
+
Allow once →
|
|
124
|
+
</button>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onClick={() => resolveApproval(message.call.id, false)}
|
|
128
|
+
className="text-muted transition-colors hover:text-ink"
|
|
129
|
+
>
|
|
130
|
+
Deny
|
|
131
|
+
</button>
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{open && (
|
|
137
|
+
<div className="animate-fade space-y-2 pb-1.5 pl-[18px] pt-0.5">
|
|
138
|
+
{command && (
|
|
139
|
+
<pre className="thin-scroll max-h-40 overflow-auto whitespace-pre-wrap break-words font-mono text-[11.5px] leading-relaxed text-ink">
|
|
140
|
+
$ {command}
|
|
141
|
+
</pre>
|
|
142
|
+
)}
|
|
143
|
+
{content && (
|
|
144
|
+
<pre className="thin-scroll max-h-48 overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed text-muted">
|
|
145
|
+
{content.length > 4000 ? `${content.slice(0, 4000)}\n… ${content.length - 4000} more characters` : content}
|
|
146
|
+
</pre>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{message.result !== undefined && (
|
|
150
|
+
<div>
|
|
151
|
+
<p className="mb-1 flex items-center gap-3 text-[11px] text-faint">
|
|
152
|
+
<span>{message.status === 'error' ? 'Error' : 'Output'}</span>
|
|
153
|
+
<button type="button" onClick={copyOutput} className="transition-colors hover:text-ink">
|
|
154
|
+
{copied ? 'Copied' : 'Copy'}
|
|
155
|
+
</button>
|
|
156
|
+
</p>
|
|
157
|
+
<pre
|
|
158
|
+
className={cn(
|
|
159
|
+
'thin-scroll max-h-72 overflow-auto whitespace-pre-wrap break-words font-mono text-[11.5px] leading-[1.65]',
|
|
160
|
+
message.status === 'error' ? 'text-danger' : 'text-muted',
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
{message.result || '(no output)'}
|
|
164
|
+
</pre>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{message.result === undefined && !command && !content && message.status !== 'awaiting' && (
|
|
169
|
+
<p className="text-[11.5px] text-muted">
|
|
170
|
+
{message.status === 'running' ? 'Working…' : 'No result was recorded.'}
|
|
171
|
+
</p>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FolderOpen,
|
|
3
|
+
ListChecks,
|
|
4
|
+
Menu,
|
|
5
|
+
Moon,
|
|
6
|
+
PanelLeftOpen,
|
|
7
|
+
Rocket,
|
|
8
|
+
Search,
|
|
9
|
+
Settings,
|
|
10
|
+
Sparkles,
|
|
11
|
+
Sun,
|
|
12
|
+
} from 'lucide-react'
|
|
13
|
+
import type { ChatSession } from '../types'
|
|
14
|
+
import { useSettings } from '../store/settings'
|
|
15
|
+
import { useUi } from '../store/ui'
|
|
16
|
+
import { useWorkspace } from '../store/workspace'
|
|
17
|
+
import { useActiveModel } from '../store/models'
|
|
18
|
+
import { cn } from '../lib/utils'
|
|
19
|
+
import { themeIsDark } from '../lib/themes'
|
|
20
|
+
import { LogoMark } from './Logo'
|
|
21
|
+
import { ContextBar } from './ContextBar'
|
|
22
|
+
|
|
23
|
+
export type ChatViewMode = 'chat' | 'trajectory'
|
|
24
|
+
|
|
25
|
+
const isApple = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform)
|
|
26
|
+
|
|
27
|
+
export function TopBar({
|
|
28
|
+
session,
|
|
29
|
+
view,
|
|
30
|
+
onViewChange,
|
|
31
|
+
}: {
|
|
32
|
+
session: ChatSession | null
|
|
33
|
+
view: ChatViewMode
|
|
34
|
+
onViewChange: (view: ChatViewMode) => void
|
|
35
|
+
}) {
|
|
36
|
+
const settings = useSettings()
|
|
37
|
+
const update = useSettings((state) => state.update)
|
|
38
|
+
const sidebarCollapsed = useUi((state) => state.sidebarCollapsed)
|
|
39
|
+
const toggleSidebar = useUi((state) => state.toggleSidebar)
|
|
40
|
+
const setMobileOpen = useUi((state) => state.setMobileSidebarOpen)
|
|
41
|
+
const setPaletteOpen = useUi((state) => state.setPaletteOpen)
|
|
42
|
+
const openSettings = useUi((state) => state.openSettings)
|
|
43
|
+
const workspaceOpen = useUi((state) => state.workspaceOpen)
|
|
44
|
+
const setWorkspaceOpen = useUi((state) => state.setWorkspaceOpen)
|
|
45
|
+
const setDeployOpen = useUi((state) => state.setDeployOpen)
|
|
46
|
+
const workspace = useWorkspace((state) => state.info)
|
|
47
|
+
const activeModel = useActiveModel()
|
|
48
|
+
|
|
49
|
+
// Go through the theme registry so Night Owl, Claude Code, Solar and plugin themes count as dark.
|
|
50
|
+
const isDark = themeIsDark(
|
|
51
|
+
settings.theme,
|
|
52
|
+
typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches,
|
|
53
|
+
)
|
|
54
|
+
const hasConversation = Boolean(session && session.messages.length > 0)
|
|
55
|
+
const turns = session?.messages.filter((message) => message.role === 'user').length ?? 0
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<header className="relative flex h-12 shrink-0 items-center gap-2 border-b border-line/60 bg-panel/80 px-3 backdrop-blur-xl sm:px-4">
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="icon-button lg:hidden"
|
|
62
|
+
onClick={() => setMobileOpen(true)}
|
|
63
|
+
aria-label="Open menu"
|
|
64
|
+
>
|
|
65
|
+
<Menu size={18} />
|
|
66
|
+
</button>
|
|
67
|
+
|
|
68
|
+
{sidebarCollapsed && (
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
className="icon-button hidden lg:inline-flex"
|
|
72
|
+
onClick={toggleSidebar}
|
|
73
|
+
title="Expand sidebar"
|
|
74
|
+
aria-label="Expand sidebar"
|
|
75
|
+
>
|
|
76
|
+
<PanelLeftOpen size={17} />
|
|
77
|
+
</button>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
81
|
+
{!session && <LogoMark size={19} className="lg:hidden" />}
|
|
82
|
+
{workspace.available && (
|
|
83
|
+
<>
|
|
84
|
+
<span className="hidden max-w-[120px] shrink-0 truncate text-[13px] text-faint md:inline">{workspace.name}</span>
|
|
85
|
+
<span className="hidden text-faint/50 md:inline">/</span>
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
<h1 className="min-w-0 truncate text-[13.5px] font-medium tracking-[-0.01em] text-ink">
|
|
89
|
+
{session?.title ?? 'New conversation'}
|
|
90
|
+
</h1>
|
|
91
|
+
{hasConversation && (
|
|
92
|
+
<span className="hidden shrink-0 items-center gap-1 rounded-md border border-line bg-canvas px-1.5 py-0.5 text-[10.5px] font-medium text-muted sm:inline-flex">
|
|
93
|
+
{settings.mode === 'plan' ? <ListChecks size={11} /> : <Sparkles size={11} />}
|
|
94
|
+
{settings.mode === 'plan' ? 'Plan mode' : 'Build mode'}
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
{hasConversation && (
|
|
98
|
+
<span className="hidden shrink-0 text-[11.5px] tabular-nums text-faint md:inline">
|
|
99
|
+
{turns} turn{turns === 1 ? '' : 's'}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{hasConversation && (
|
|
105
|
+
<div className="absolute left-1/2 hidden -translate-x-1/2 items-center rounded-lg bg-raised p-0.5 md:inline-flex">
|
|
106
|
+
{(['chat', 'trajectory'] as const).map((value) => (
|
|
107
|
+
<button
|
|
108
|
+
key={value}
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={() => onViewChange(value)}
|
|
111
|
+
aria-pressed={view === value}
|
|
112
|
+
className={cn(
|
|
113
|
+
'h-6 rounded-md px-3 text-[11.5px] font-medium capitalize transition-all duration-200',
|
|
114
|
+
view === value ? 'bg-panel text-ink shadow-pill' : 'text-muted hover:text-ink',
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{value}
|
|
118
|
+
</button>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
124
|
+
{hasConversation && (
|
|
125
|
+
<div className="mr-3 hidden sm:block">
|
|
126
|
+
<ContextBar messages={session?.messages ?? []} model={activeModel} sessionId={session?.id ?? null} />
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={() => setWorkspaceOpen(!workspaceOpen)}
|
|
132
|
+
title={workspace.available ? workspace.root : 'Choose a workspace folder'}
|
|
133
|
+
className={cn(
|
|
134
|
+
'mr-1 hidden min-w-0 items-center gap-1.5 rounded-lg border px-2.5 py-1 text-[12px] transition-all duration-150 sm:inline-flex',
|
|
135
|
+
workspaceOpen
|
|
136
|
+
? 'border-accent/30 bg-accent-soft text-accent'
|
|
137
|
+
: 'border-line text-muted hover:bg-raised/60 hover:text-ink',
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
<FolderOpen size={13} className="shrink-0" />
|
|
141
|
+
<span className="max-w-[140px] truncate">{workspace.available ? workspace.name : 'No folder'}</span>
|
|
142
|
+
</button>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
className="icon-button hidden sm:inline-flex"
|
|
146
|
+
onClick={() => setDeployOpen(true)}
|
|
147
|
+
title="Deploy the open workspace or another project"
|
|
148
|
+
aria-label="Deploy"
|
|
149
|
+
>
|
|
150
|
+
<Rocket size={16} strokeWidth={1.7} />
|
|
151
|
+
</button>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
className="icon-button"
|
|
155
|
+
onClick={() => setPaletteOpen(true)}
|
|
156
|
+
title={`Search and commands (${isApple ? '⌘' : 'Ctrl'} K)`}
|
|
157
|
+
aria-label="Open search and commands"
|
|
158
|
+
>
|
|
159
|
+
<Search size={16} />
|
|
160
|
+
</button>
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
className="icon-button hidden sm:inline-flex"
|
|
164
|
+
onClick={() => update({ theme: isDark ? 'light' : 'dark' })}
|
|
165
|
+
title={isDark ? 'Switch to light theme' : 'Switch to dark theme'}
|
|
166
|
+
aria-label="Toggle theme"
|
|
167
|
+
>
|
|
168
|
+
{isDark ? <Sun size={16} /> : <Moon size={16} />}
|
|
169
|
+
</button>
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
className="icon-button"
|
|
173
|
+
onClick={() => openSettings('general')}
|
|
174
|
+
title="Settings"
|
|
175
|
+
aria-label="Open settings"
|
|
176
|
+
>
|
|
177
|
+
<Settings size={16} />
|
|
178
|
+
</button>
|
|
179
|
+
</div>
|
|
180
|
+
</header>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
AlertCircle,
|
|
4
|
+
Check,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
FileText,
|
|
7
|
+
SquareTerminal,
|
|
8
|
+
UserRound,
|
|
9
|
+
Wrench,
|
|
10
|
+
} from 'lucide-react'
|
|
11
|
+
import type { ChatSession, Message } from '../types'
|
|
12
|
+
import { toolLabel, toolSummary } from '../agent/tools'
|
|
13
|
+
import { cn } from '../lib/utils'
|
|
14
|
+
import { LogoMark } from './Logo'
|
|
15
|
+
|
|
16
|
+
function timeOf(timestamp: number) {
|
|
17
|
+
return new Date(timestamp).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function preview(text: string, length = 180) {
|
|
21
|
+
const clean = text.replace(/\s+/g, ' ').trim()
|
|
22
|
+
return clean.length > length ? `${clean.slice(0, length)}…` : clean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function statusTone(message: Extract<Message, { role: 'tool' }>) {
|
|
26
|
+
switch (message.status) {
|
|
27
|
+
case 'running':
|
|
28
|
+
return { label: 'Running', className: 'text-accent', dot: 'bg-accent animate-pulse' }
|
|
29
|
+
case 'awaiting':
|
|
30
|
+
return { label: 'Needs approval', className: 'text-warning', dot: 'bg-warning' }
|
|
31
|
+
case 'done':
|
|
32
|
+
return { label: 'Done', className: 'text-success', dot: 'bg-success' }
|
|
33
|
+
case 'denied':
|
|
34
|
+
return { label: 'Denied', className: 'text-muted', dot: 'bg-faint' }
|
|
35
|
+
default:
|
|
36
|
+
return { label: 'Failed', className: 'text-danger', dot: 'bg-danger' }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function TrajectoryView({ session }: { session: ChatSession }) {
|
|
41
|
+
const events = useMemo(() => session.messages, [session.messages])
|
|
42
|
+
const toolCount = events.filter((message) => message.role === 'tool').length
|
|
43
|
+
|
|
44
|
+
if (events.length === 0) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex h-full items-center justify-center px-6">
|
|
47
|
+
<div className="text-center">
|
|
48
|
+
<Wrench size={20} className="mx-auto text-faint" />
|
|
49
|
+
<p className="mt-2 text-[13px] font-medium text-ink">No trajectory yet</p>
|
|
50
|
+
<p className="mt-1 text-[11.5px] text-muted">Send a message to see every step here.</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="thin-scroll h-full overflow-y-auto">
|
|
58
|
+
<div className="mx-auto w-full max-w-[800px] px-4 pb-12 pt-7 sm:px-6">
|
|
59
|
+
<div className="mb-5 flex items-center justify-between gap-3 border-b border-line pb-3">
|
|
60
|
+
<div>
|
|
61
|
+
<p className="text-[13px] font-medium text-ink">Trajectory</p>
|
|
62
|
+
<p className="mt-0.5 text-[11px] text-muted">
|
|
63
|
+
{events.length} step{events.length === 1 ? '' : 's'} · {toolCount} tool call{toolCount === 1 ? '' : 's'}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
<span className="rounded-full border border-line px-2 py-0.5 text-[10px] text-faint">Read-only view</span>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="relative">
|
|
70
|
+
<span className="absolute bottom-3 left-[13px] top-3 w-px bg-line" aria-hidden="true" />
|
|
71
|
+
<div className="space-y-4">
|
|
72
|
+
{events.map((message) => {
|
|
73
|
+
if (message.role === 'user') {
|
|
74
|
+
return (
|
|
75
|
+
<div key={message.id} className="relative flex gap-3">
|
|
76
|
+
<span className="relative z-10 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-line bg-panel text-muted">
|
|
77
|
+
<UserRound size={13} />
|
|
78
|
+
</span>
|
|
79
|
+
<div className="min-w-0 flex-1 rounded-xl border border-line bg-panel px-3 py-2">
|
|
80
|
+
<div className="flex items-center justify-between gap-2">
|
|
81
|
+
<span className="text-[11.5px] font-medium text-ink">You</span>
|
|
82
|
+
<span className="text-[9.5px] tabular-nums text-faint">{timeOf(message.createdAt)}</span>
|
|
83
|
+
</div>
|
|
84
|
+
<p className="mt-1 line-clamp-3 text-[12px] leading-relaxed text-muted">{message.content || 'Attachment only'}</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (message.role === 'assistant') {
|
|
91
|
+
const hasText = Boolean(message.content.trim() || message.reasoning.trim())
|
|
92
|
+
return (
|
|
93
|
+
<div key={message.id} className="relative flex gap-3">
|
|
94
|
+
<span className="relative z-10 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-line bg-panel">
|
|
95
|
+
<LogoMark size={15} />
|
|
96
|
+
</span>
|
|
97
|
+
<div className="min-w-0 flex-1 rounded-xl border border-line bg-panel px-3 py-2">
|
|
98
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
99
|
+
<span className="flex min-w-0 items-center gap-1.5 text-[11.5px] font-medium text-ink">
|
|
100
|
+
OpenSol
|
|
101
|
+
{message.model && (
|
|
102
|
+
<span className="truncate text-[10px] font-normal text-faint">{message.model}</span>
|
|
103
|
+
)}
|
|
104
|
+
</span>
|
|
105
|
+
<span className="flex items-center gap-1.5 text-[9.5px] tabular-nums text-faint">
|
|
106
|
+
{message.stopped && <span className="text-muted">Stopped</span>}
|
|
107
|
+
{timeOf(message.createdAt)}
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
{hasText ? (
|
|
111
|
+
<p className="mt-1 line-clamp-4 whitespace-pre-wrap text-[12px] leading-relaxed text-muted">
|
|
112
|
+
{preview(message.content || message.reasoning)}
|
|
113
|
+
</p>
|
|
114
|
+
) : (
|
|
115
|
+
<p className="mt-1 text-[11px] italic text-faint">Model turn with no text.</p>
|
|
116
|
+
)}
|
|
117
|
+
{message.error && (
|
|
118
|
+
<p className="mt-1.5 flex items-start gap-1.5 text-[11px] leading-relaxed text-danger">
|
|
119
|
+
<AlertCircle size={11} className="mt-0.5 shrink-0" />
|
|
120
|
+
{message.error}
|
|
121
|
+
</p>
|
|
122
|
+
)}
|
|
123
|
+
{message.toolCalls.length > 0 && (
|
|
124
|
+
<p className="mt-1.5 text-[10px] text-faint">
|
|
125
|
+
{message.toolCalls.length} tool call{message.toolCalls.length === 1 ? '' : 's'} in this round
|
|
126
|
+
</p>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const status = statusTone(message)
|
|
134
|
+
const isShell = message.call.name === 'shell'
|
|
135
|
+
const isFile = ['read_file', 'write_file', 'edit_file', 'rewrite_file'].includes(message.call.name)
|
|
136
|
+
const Icon = isShell ? SquareTerminal : isFile ? FileText : Wrench
|
|
137
|
+
return (
|
|
138
|
+
<div key={message.id} className="relative flex gap-3">
|
|
139
|
+
<span className="relative z-10 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-line bg-panel text-muted">
|
|
140
|
+
<Icon size={12} />
|
|
141
|
+
</span>
|
|
142
|
+
<div className="min-w-0 flex-1 rounded-xl border border-line/80 bg-raised/25 px-3 py-2">
|
|
143
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
144
|
+
<span className="text-[11.5px] font-medium text-ink">{toolLabel(message.call.name)}</span>
|
|
145
|
+
<span className="min-w-0 flex-1 truncate font-mono text-[10.5px] text-muted">
|
|
146
|
+
{toolSummary(message.call.name, message.call.args) || '—'}
|
|
147
|
+
</span>
|
|
148
|
+
<span className={cn('flex shrink-0 items-center gap-1 text-[10px] font-medium', status.className)}>
|
|
149
|
+
{message.status === 'done' ? (
|
|
150
|
+
<Check size={10} />
|
|
151
|
+
) : (
|
|
152
|
+
<span className={cn('h-1.5 w-1.5 rounded-full', status.dot)} />
|
|
153
|
+
)}
|
|
154
|
+
{status.label}
|
|
155
|
+
{message.durationMs !== undefined && message.status === 'done'
|
|
156
|
+
? ` · ${message.durationMs < 1000 ? `${message.durationMs}ms` : `${(message.durationMs / 1000).toFixed(1)}s`}`
|
|
157
|
+
: ''}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
{message.result && (
|
|
161
|
+
<p className="mt-1.5 line-clamp-2 whitespace-pre-wrap break-words font-mono text-[10.5px] leading-relaxed text-faint">
|
|
162
|
+
{preview(message.result, 160)}
|
|
163
|
+
</p>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
})}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<p className="mt-6 flex items-center justify-center gap-1 text-[10px] text-faint">
|
|
173
|
+
End of trajectory <ChevronRight size={10} />
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|