@workerdeck/ui 0.22.0 → 1.0.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/build/{SessionPanel-DgwjH4Ve.mjs → SessionPanel-DDgBkHsi.mjs} +2196 -3364
- package/build/SessionPanel-DDgBkHsi.mjs.map +1 -0
- package/build/SessionPanel-E_L_ldQ5.d.mts +207 -0
- package/build/format-B8ZM9LPy.d.mts +16 -0
- package/build/format.d.mts +8 -75
- package/build/format.mjs +2 -2
- package/build/index.d.mts +63 -1138
- package/build/index.mjs +4 -300
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +80 -24
- package/build/{status-BE-zg88x.mjs → status-C0HSLZas.mjs} +14 -85
- package/build/status-C0HSLZas.mjs.map +1 -0
- package/build/workspace.d.mts +9 -124
- package/build/workspace.mjs +13 -143
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/CodeEditor.tsx +31 -103
- package/src/components/agent/Composer.tsx +114 -333
- package/src/components/agent/ContextDialog.tsx +20 -41
- package/src/components/agent/ContextRing.tsx +6 -28
- package/src/components/agent/Conversation.tsx +21 -33
- package/src/components/agent/EditorTabs.tsx +39 -67
- package/src/components/agent/EngineIcon.tsx +29 -72
- package/src/components/agent/EntryIcon.tsx +16 -0
- package/src/components/agent/FileCard.tsx +10 -16
- package/src/components/agent/FileTree.tsx +52 -125
- package/src/components/agent/FileViewer.tsx +30 -60
- package/src/components/agent/HostFilesDialog.tsx +40 -69
- package/src/components/agent/Loader.tsx +3 -24
- package/src/components/agent/McpDialog.tsx +64 -137
- package/src/components/agent/Message.tsx +2 -21
- package/src/components/agent/ModelSelect.tsx +33 -62
- package/src/components/agent/PermissionModeSelect.tsx +21 -79
- package/src/components/agent/PermissionPrompt.tsx +59 -63
- package/src/components/agent/ProjectIcon.tsx +228 -93
- package/src/components/agent/PromptTokenText.tsx +10 -16
- package/src/components/agent/QuestionPrompt.tsx +43 -64
- package/src/components/agent/Reasoning.tsx +13 -16
- package/src/components/agent/Response.tsx +4 -6
- package/src/components/agent/Scrubber.tsx +31 -80
- package/src/components/agent/SessionBrowser.tsx +64 -243
- package/src/components/agent/SessionEmptyState.tsx +10 -40
- package/src/components/agent/SessionInfoDialog.tsx +46 -78
- package/src/components/agent/SessionItem.tsx +53 -320
- package/src/components/agent/SessionList.tsx +23 -39
- package/src/components/agent/SessionPanel.tsx +345 -923
- package/src/components/agent/SessionStatusIcon.tsx +14 -28
- package/src/components/agent/SessionSteps.tsx +34 -187
- package/src/components/agent/SessionWorkspace.tsx +108 -215
- package/src/components/agent/SkillsDialog.tsx +35 -80
- package/src/components/agent/StatusBar.tsx +51 -136
- package/src/components/agent/SubagentStrip.tsx +12 -77
- package/src/components/agent/ToolCallCard.tsx +94 -135
- package/src/components/agent/Transcript.tsx +103 -1288
- package/src/components/agent/TranscriptItemView.tsx +155 -0
- package/src/components/agent/TranscriptRows.tsx +418 -0
- package/src/components/agent/UsageDialog.tsx +7 -29
- package/src/components/agent/UsageMeters.tsx +18 -73
- package/src/components/agent/pulse.tsx +9 -32
- package/src/components/agent/scrubber-marks.ts +33 -103
- package/src/components/agent/status.ts +1 -5
- package/src/components/agent/tool-result-fetch.tsx +3 -25
- package/src/components/agent/tool-result-image.tsx +32 -111
- package/src/components/agent/transcript-rows.ts +41 -118
- package/src/components/agent/transcript-variant.tsx +3 -80
- package/src/components/agent/use-height-epoch.ts +10 -24
- package/src/components/agent/use-path-links.ts +45 -63
- package/src/components/agent/use-subagent-frame.ts +106 -0
- package/src/components/agent/use-transcript-jumps.ts +49 -107
- package/src/components/prompt-area/animated-placeholder.tsx +6 -5
- package/src/components/prompt-area/clipboard-helpers.ts +26 -21
- package/src/components/prompt-area/cursor-helpers.ts +79 -37
- package/src/components/prompt-area/dom-helpers.ts +97 -69
- package/src/components/prompt-area/file-strip.tsx +90 -80
- package/src/components/prompt-area/html-to-markdown.ts +67 -46
- package/src/components/prompt-area/image-strip.tsx +8 -18
- package/src/components/prompt-area/index.ts +2 -15
- package/src/components/prompt-area/prompt-area-engine.ts +100 -115
- package/src/components/prompt-area/prompt-area-list-ops.ts +66 -59
- package/src/components/prompt-area/prompt-area.tsx +31 -38
- package/src/components/prompt-area/remove-button.tsx +3 -9
- package/src/components/prompt-area/segment-helpers.ts +4 -12
- package/src/components/prompt-area/trigger-popover.tsx +15 -27
- package/src/components/prompt-area/trigger-presets.ts +0 -10
- package/src/components/prompt-area/types.ts +3 -8
- package/src/components/prompt-area/use-chip-editing.ts +305 -0
- package/src/components/prompt-area/use-markdown-mode.ts +7 -17
- package/src/components/prompt-area/use-prompt-area-events.ts +80 -86
- package/src/components/prompt-area/use-prompt-area-keydown.ts +598 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +5 -23
- package/src/components/prompt-area/use-prompt-area.ts +170 -896
- package/src/components/prompt-area/use-trigger-search.ts +21 -19
- package/src/components/terminal/PermissionPrompt.tsx +56 -59
- package/src/components/terminal/QuestionPrompt.tsx +28 -79
- package/src/components/terminal/StatusLine.tsx +13 -63
- package/src/components/terminal/TerminalTranscript.tsx +44 -160
- package/src/components/terminal/affordances.tsx +68 -98
- package/src/components/terminal/blocks.ts +20 -156
- package/src/components/terminal/diff.tsx +19 -67
- package/src/components/terminal/height.ts +163 -313
- package/src/components/terminal/image-box.ts +0 -44
- package/src/components/terminal/items.tsx +150 -305
- package/src/components/terminal/markdown.tsx +36 -95
- package/src/components/terminal/press.tsx +26 -53
- package/src/components/terminal/prompt.tsx +40 -170
- package/src/components/terminal/result-preview.ts +8 -56
- package/src/components/terminal/row.tsx +11 -98
- package/src/components/terminal/scrubber.tsx +108 -401
- package/src/components/terminal/surface.tsx +6 -54
- package/src/components/terminal/todos.ts +66 -0
- package/src/components/terminal/tool-run.ts +21 -192
- package/src/components/ui/AlertDialog.tsx +8 -23
- package/src/components/ui/Badge.tsx +18 -29
- package/src/components/ui/Button.tsx +7 -15
- package/src/components/ui/Card.tsx +2 -13
- package/src/components/ui/CodeBlock.tsx +10 -37
- package/src/components/ui/CopyButton.tsx +10 -23
- package/src/components/ui/Dialog.tsx +23 -36
- package/src/components/ui/Empty.tsx +6 -29
- package/src/components/ui/Input.tsx +14 -16
- package/src/components/ui/Menu.tsx +6 -18
- package/src/components/ui/PortalScope.tsx +1 -21
- package/src/components/ui/ProgressRing.tsx +8 -19
- package/src/components/ui/Select.tsx +18 -31
- package/src/components/ui/Sonner.tsx +1 -2
- package/src/components/ui/Spinner.tsx +1 -1
- package/src/components/ui/Splitter.tsx +30 -64
- package/src/components/ui/Textarea.tsx +2 -5
- package/src/components/ui/Tooltip.tsx +3 -13
- package/src/format.ts +0 -9
- package/src/index.ts +16 -113
- package/src/lib/clipboard.ts +8 -28
- package/src/lib/css.ts +8 -0
- package/src/lib/format.ts +101 -86
- package/src/lib/plan-request.ts +16 -0
- package/src/lib/status.ts +45 -95
- package/src/lib/tool-icon.ts +39 -47
- package/src/lib/utils.ts +1 -3
- package/src/styles/terminal.css +81 -49
- package/src/styles/theme.css +136 -19
- package/src/workspace.ts +1 -24
- package/build/SessionPanel-13l25ubU.d.mts +0 -609
- package/build/SessionPanel-DgwjH4Ve.mjs.map +0 -1
- package/build/format-ljc3lKpA.d.mts +0 -59
- package/build/status-BE-zg88x.mjs.map +0 -1
|
@@ -1,69 +1,34 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'
|
|
2
2
|
import type { UseHostFileSearchResult, UseHostFileTreeResult } from '@workerdeck/react'
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
ChevronRight,
|
|
6
|
-
File,
|
|
7
|
-
Folder,
|
|
8
|
-
FolderOpen,
|
|
9
|
-
Link2,
|
|
10
|
-
PanelLeftClose,
|
|
11
|
-
RefreshCw,
|
|
12
|
-
Search,
|
|
13
|
-
X,
|
|
14
|
-
} from 'lucide-react'
|
|
3
|
+
import type { HostFileMatch } from '@workerdeck/protocol'
|
|
4
|
+
import { ChevronRight, File, Folder, FolderOpen, PanelLeftClose, RefreshCw, Search, X } from 'lucide-react'
|
|
15
5
|
import { cn } from '../../lib/utils.ts'
|
|
16
6
|
import { Button } from '../ui/Button.tsx'
|
|
17
7
|
import { Input } from '../ui/Input.tsx'
|
|
18
8
|
import { Spinner } from '../ui/Spinner.tsx'
|
|
9
|
+
import { EntryIcon } from './EntryIcon.tsx'
|
|
19
10
|
|
|
20
11
|
export interface FileTreeProps {
|
|
21
|
-
/** Tree state from `useHostFileTree` — this component holds none of its own. */
|
|
22
12
|
tree: UseHostFileTreeResult
|
|
23
|
-
/** Optional search from `useHostFileSearch`; omit and the box is not offered. */
|
|
24
13
|
search?: UseHostFileSearchResult
|
|
25
|
-
/** Path of the focused file, highlighted in the tree. */
|
|
26
14
|
activePath?: string
|
|
27
15
|
onOpenFile: (path: string) => void
|
|
28
|
-
/** Offered as a button in the header when given. */
|
|
29
16
|
onCollapse?: () => void
|
|
30
17
|
style?: CSSProperties
|
|
31
18
|
className?: string
|
|
32
19
|
}
|
|
33
20
|
|
|
34
|
-
/** How far one level of nesting indents, in pixels. Inline rather than a Tailwind
|
|
35
|
-
* class because depth is a number at runtime and `pl-${n}` is not a class. */
|
|
36
21
|
const INDENT = 12
|
|
37
22
|
|
|
38
|
-
|
|
39
|
-
* The workspace's left rail: an expandable tree of the session's project,
|
|
40
|
-
* with a search box over the same fuzzy route `@file` completion uses.
|
|
41
|
-
*
|
|
42
|
-
* Presentational by construction — every piece of state it renders comes from
|
|
43
|
-
* the hooks in `@workerdeck/react`, and the only thing it owns is the search
|
|
44
|
-
* query, which is the text in its own input.
|
|
45
|
-
*
|
|
46
|
-
* Searching replaces the tree with matches rather than filtering it: the route
|
|
47
|
-
* answers with paths from all over the project, and threading those back into
|
|
48
|
-
* tree positions would mean expanding a dozen directories to show six results.
|
|
49
|
-
*/
|
|
50
|
-
export function FileTree({
|
|
51
|
-
tree,
|
|
52
|
-
search,
|
|
53
|
-
activePath,
|
|
54
|
-
onOpenFile,
|
|
55
|
-
onCollapse,
|
|
56
|
-
style,
|
|
57
|
-
className,
|
|
58
|
-
}: FileTreeProps) {
|
|
23
|
+
export function FileTree({ tree, search, activePath, onOpenFile, onCollapse, style, className }: FileTreeProps) {
|
|
59
24
|
const [query, setQuery] = useState('')
|
|
60
25
|
const [matches, setMatches] = useState<HostFileMatch[] | undefined>()
|
|
61
26
|
const searching = query.trim().length > 0
|
|
62
27
|
|
|
63
|
-
// Debounced, and only while there is something to search for — an empty box
|
|
64
|
-
// means "show me the tree again", not "match everything".
|
|
65
28
|
useEffect(() => {
|
|
66
|
-
if (!search?.available)
|
|
29
|
+
if (!search?.available) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
67
32
|
const q = query.trim()
|
|
68
33
|
if (!q) {
|
|
69
34
|
setMatches(undefined)
|
|
@@ -72,7 +37,9 @@ export function FileTree({
|
|
|
72
37
|
const controller = new AbortController()
|
|
73
38
|
const timer = setTimeout(() => {
|
|
74
39
|
void search.search(q, { limit: 60, signal: controller.signal }).then((found) => {
|
|
75
|
-
if (!controller.signal.aborted)
|
|
40
|
+
if (!controller.signal.aborted) {
|
|
41
|
+
setMatches(found)
|
|
42
|
+
}
|
|
76
43
|
})
|
|
77
44
|
}, 150)
|
|
78
45
|
return () => {
|
|
@@ -82,71 +49,61 @@ export function FileTree({
|
|
|
82
49
|
}, [search, query])
|
|
83
50
|
|
|
84
51
|
return (
|
|
85
|
-
<div
|
|
86
|
-
|
|
87
|
-
style={style}
|
|
88
|
-
className={cn('flex min-h-0 min-w-0 flex-col bg-surface', className)}>
|
|
89
|
-
<div className='flex items-center gap-1 px-2 pt-2'>
|
|
52
|
+
<div data-slot="file-tree" style={style} className={cn('flex min-h-0 min-w-0 flex-col bg-surface', className)}>
|
|
53
|
+
<div className="flex items-center gap-1 px-2 pt-2">
|
|
90
54
|
{search?.available ? (
|
|
91
|
-
<div className=
|
|
92
|
-
<Search className=
|
|
55
|
+
<div className="relative min-w-0 flex-1">
|
|
56
|
+
<Search className="absolute top-1/2 left-2 size-3.5 -translate-y-1/2 text-fg-4" />
|
|
93
57
|
<Input
|
|
94
58
|
value={query}
|
|
95
59
|
onChange={(e) => setQuery(e.target.value)}
|
|
96
|
-
placeholder=
|
|
97
|
-
className=
|
|
60
|
+
placeholder="Search files…"
|
|
61
|
+
className="h-7 pr-7 pl-7 text-label"
|
|
98
62
|
spellCheck={false}
|
|
99
63
|
/>
|
|
100
64
|
{searching ? (
|
|
101
65
|
<button
|
|
102
|
-
type=
|
|
66
|
+
type="button"
|
|
103
67
|
onClick={() => setQuery('')}
|
|
104
|
-
aria-label=
|
|
105
|
-
className=
|
|
106
|
-
|
|
68
|
+
aria-label="Clear search"
|
|
69
|
+
className="absolute top-1/2 right-1.5 -translate-y-1/2 text-fg-4 transition-colors hover:text-fg-2"
|
|
70
|
+
>
|
|
71
|
+
<X className="size-3.5" />
|
|
107
72
|
</button>
|
|
108
73
|
) : null}
|
|
109
74
|
</div>
|
|
110
75
|
) : (
|
|
111
|
-
<span className=
|
|
76
|
+
<span className="min-w-0 flex-1 truncate px-1 text-label text-fg-4">{tree.root}</span>
|
|
112
77
|
)}
|
|
113
|
-
<Button
|
|
114
|
-
|
|
115
|
-
size='icon-sm'
|
|
116
|
-
aria-label='Refresh the file tree'
|
|
117
|
-
onClick={() => tree.refresh()}>
|
|
118
|
-
<RefreshCw className='size-3.5 text-fg-3' />
|
|
78
|
+
<Button variant="ghost" size="icon-sm" aria-label="Refresh the file tree" onClick={() => tree.refresh()}>
|
|
79
|
+
<RefreshCw className="size-3.5 text-fg-3" />
|
|
119
80
|
</Button>
|
|
120
81
|
{onCollapse ? (
|
|
121
|
-
<Button variant=
|
|
122
|
-
<PanelLeftClose className=
|
|
82
|
+
<Button variant="ghost" size="icon-sm" aria-label="Hide project files" onClick={onCollapse}>
|
|
83
|
+
<PanelLeftClose className="size-3.5 text-fg-3" />
|
|
123
84
|
</Button>
|
|
124
85
|
) : null}
|
|
125
86
|
</div>
|
|
126
87
|
|
|
127
|
-
<div className=
|
|
88
|
+
<div className="min-h-0 flex-1 overflow-auto px-1 py-1">
|
|
128
89
|
{tree.error ? (
|
|
129
|
-
<p className=
|
|
90
|
+
<p className="px-2 py-3 text-body-sm text-danger">{tree.error}</p>
|
|
130
91
|
) : searching ? (
|
|
131
92
|
matches === undefined ? (
|
|
132
|
-
<div className=
|
|
133
|
-
<Spinner className=
|
|
93
|
+
<div className="py-6 text-center">
|
|
94
|
+
<Spinner className="size-4 text-fg-4" />
|
|
134
95
|
</div>
|
|
135
96
|
) : matches.length === 0 ? (
|
|
136
|
-
<p className=
|
|
97
|
+
<p className="px-2 py-6 text-center text-body-sm text-fg-4">No matching files.</p>
|
|
137
98
|
) : (
|
|
138
99
|
<ul>
|
|
139
100
|
{matches.map((match) => (
|
|
140
101
|
<li key={match.path}>
|
|
141
102
|
<Row
|
|
142
103
|
label={fileName(match.relative)}
|
|
143
|
-
// The directory, dimmed and truncated separately, so a deep
|
|
144
|
-
// path cannot push the filename out of the row — a rail full
|
|
145
|
-
// of `apps/ios/DerivedData/B…` says nothing about which file
|
|
146
|
-
// each hit is.
|
|
147
104
|
detail={directoryOf(match.relative)}
|
|
148
105
|
title={match.relative}
|
|
149
|
-
icon={<File className=
|
|
106
|
+
icon={<File className="size-3.5 shrink-0 text-fg-4" />}
|
|
150
107
|
active={match.path === activePath}
|
|
151
108
|
onClick={() => onOpenFile(match.path)}
|
|
152
109
|
/>
|
|
@@ -155,15 +112,15 @@ export function FileTree({
|
|
|
155
112
|
</ul>
|
|
156
113
|
)
|
|
157
114
|
) : tree.loading ? (
|
|
158
|
-
<div className=
|
|
159
|
-
<Spinner className=
|
|
115
|
+
<div className="py-6 text-center">
|
|
116
|
+
<Spinner className="size-4 text-fg-4" />
|
|
160
117
|
</div>
|
|
161
118
|
) : tree.rows.length === 0 ? (
|
|
162
|
-
<p className=
|
|
119
|
+
<p className="px-2 py-6 text-center text-body-sm text-fg-4">This project is empty.</p>
|
|
163
120
|
) : (
|
|
164
|
-
<ul role=
|
|
121
|
+
<ul role="tree" aria-label="Project files">
|
|
165
122
|
{tree.rows.map((row) => (
|
|
166
|
-
<li key={row.entry.path} role=
|
|
123
|
+
<li key={row.entry.path} role="treeitem" aria-expanded={row.expanded} aria-level={row.depth + 1}>
|
|
167
124
|
<Row
|
|
168
125
|
label={row.entry.name}
|
|
169
126
|
indent={row.depth}
|
|
@@ -171,11 +128,11 @@ export function FileTree({
|
|
|
171
128
|
icon={
|
|
172
129
|
row.entry.type === 'dir' ? (
|
|
173
130
|
row.loading ? (
|
|
174
|
-
<Spinner className=
|
|
131
|
+
<Spinner className="size-3.5 shrink-0 text-fg-4" />
|
|
175
132
|
) : row.expanded ? (
|
|
176
|
-
<FolderOpen className=
|
|
133
|
+
<FolderOpen className="size-3.5 shrink-0 text-accent" />
|
|
177
134
|
) : (
|
|
178
|
-
<Folder className=
|
|
135
|
+
<Folder className="size-3.5 shrink-0 text-accent" />
|
|
179
136
|
)
|
|
180
137
|
) : (
|
|
181
138
|
<EntryIcon type={row.entry.type} />
|
|
@@ -183,24 +140,13 @@ export function FileTree({
|
|
|
183
140
|
}
|
|
184
141
|
chevron={
|
|
185
142
|
row.entry.type === 'dir' ? (
|
|
186
|
-
<ChevronRight
|
|
187
|
-
className={cn(
|
|
188
|
-
'size-3 shrink-0 text-fg-4 transition-transform',
|
|
189
|
-
row.expanded && 'rotate-90',
|
|
190
|
-
)}
|
|
191
|
-
/>
|
|
143
|
+
<ChevronRight className={cn('size-3 shrink-0 text-fg-4 transition-transform', row.expanded && 'rotate-90')} />
|
|
192
144
|
) : undefined
|
|
193
145
|
}
|
|
194
|
-
onClick={() =>
|
|
195
|
-
row.entry.type === 'dir'
|
|
196
|
-
? tree.toggle(row.entry.path)
|
|
197
|
-
: onOpenFile(row.entry.path)
|
|
198
|
-
}
|
|
146
|
+
onClick={() => (row.entry.type === 'dir' ? tree.toggle(row.entry.path) : onOpenFile(row.entry.path))}
|
|
199
147
|
/>
|
|
200
148
|
{row.truncated ? (
|
|
201
|
-
<p
|
|
202
|
-
className='truncate py-0.5 text-label text-fg-4'
|
|
203
|
-
style={{ paddingLeft: (row.depth + 1) * INDENT + 22 }}>
|
|
149
|
+
<p className="truncate py-0.5 text-label text-fg-4" style={{ paddingLeft: (row.depth + 1) * INDENT + 22 }}>
|
|
204
150
|
More entries than the server will return — use search.
|
|
205
151
|
</p>
|
|
206
152
|
) : null}
|
|
@@ -213,8 +159,6 @@ export function FileTree({
|
|
|
213
159
|
)
|
|
214
160
|
}
|
|
215
161
|
|
|
216
|
-
/** One clickable line. A directory and a file differ only in what the click does
|
|
217
|
-
* and whether there is a chevron — visually they are the same row. */
|
|
218
162
|
function Row({
|
|
219
163
|
label,
|
|
220
164
|
detail,
|
|
@@ -226,7 +170,6 @@ function Row({
|
|
|
226
170
|
onClick,
|
|
227
171
|
}: {
|
|
228
172
|
label: string
|
|
229
|
-
/** Secondary text after the label, dimmed and the first thing to be truncated. */
|
|
230
173
|
detail?: string
|
|
231
174
|
title?: string
|
|
232
175
|
icon: ReactNode
|
|
@@ -235,54 +178,38 @@ function Row({
|
|
|
235
178
|
active?: boolean
|
|
236
179
|
onClick: () => void
|
|
237
180
|
}) {
|
|
238
|
-
// Scroll a row that became active elsewhere (a search hit, a `reveal`) into
|
|
239
|
-
// view, but never yank the list while someone is reading it — `nearest` moves
|
|
240
|
-
// the minimum and does nothing when the row is already visible.
|
|
241
181
|
const ref = useRef<HTMLButtonElement>(null)
|
|
242
182
|
useEffect(() => {
|
|
243
|
-
if (active)
|
|
183
|
+
if (active) {
|
|
184
|
+
ref.current?.scrollIntoView({ block: 'nearest' })
|
|
185
|
+
}
|
|
244
186
|
}, [active])
|
|
245
187
|
|
|
246
188
|
return (
|
|
247
189
|
<button
|
|
248
190
|
ref={ref}
|
|
249
|
-
type=
|
|
191
|
+
type="button"
|
|
250
192
|
onClick={onClick}
|
|
251
193
|
title={title ?? label}
|
|
252
194
|
style={{ paddingLeft: indent * INDENT + 4 }}
|
|
253
195
|
className={cn(
|
|
254
196
|
'flex w-full items-center gap-1 rounded py-1 pr-2 text-left transition-colors',
|
|
255
197
|
active ? 'bg-surface-hover text-fg-1' : 'text-fg-2 hover:bg-surface-hover',
|
|
256
|
-
)}
|
|
257
|
-
|
|
198
|
+
)}
|
|
199
|
+
>
|
|
200
|
+
<span className="flex size-3 shrink-0 items-center justify-center">{chevron}</span>
|
|
258
201
|
{icon}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
The rail reads in the **UI font**, never mono: it is workbench chrome
|
|
262
|
-
— a list you scan — and the editors it sits beside (VS Code, Finder)
|
|
263
|
-
all set filenames in their UI face. Mono is for content on a grid, and
|
|
264
|
-
nothing here is on one. It is also independent of `transcriptFont`,
|
|
265
|
-
which scopes to the session panel alone. */}
|
|
266
|
-
<span className='shrink-0 truncate text-label'>{label}</span>
|
|
267
|
-
{detail ? <span className='min-w-0 flex-1 truncate text-label text-fg-4'>{detail}</span> : null}
|
|
202
|
+
<span className="shrink-0 truncate text-label">{label}</span>
|
|
203
|
+
{detail ? <span className="min-w-0 flex-1 truncate text-label text-fg-4">{detail}</span> : null}
|
|
268
204
|
</button>
|
|
269
205
|
)
|
|
270
206
|
}
|
|
271
207
|
|
|
272
|
-
/** Last segment of a relative match. */
|
|
273
208
|
function fileName(relative: string): string {
|
|
274
209
|
return relative.slice(relative.lastIndexOf('/') + 1)
|
|
275
210
|
}
|
|
276
211
|
|
|
277
|
-
/** Everything before it, or `undefined` for a file at the search root. */
|
|
278
212
|
function directoryOf(relative: string): string | undefined {
|
|
279
213
|
const cut = relative.lastIndexOf('/')
|
|
280
214
|
return cut === -1 ? undefined : relative.slice(0, cut)
|
|
281
215
|
}
|
|
282
|
-
|
|
283
|
-
/** A symlink is reported as itself and never silently resolved — following it is
|
|
284
|
-
* the next request's problem, and that request is refused if it escapes the roots. */
|
|
285
|
-
function EntryIcon({ type }: { type: HostDirEntry['type'] }) {
|
|
286
|
-
if (type === 'symlink') return <Link2 className='size-3.5 shrink-0 text-fg-4' />
|
|
287
|
-
return <File className='size-3.5 shrink-0 text-fg-4' />
|
|
288
|
-
}
|
|
@@ -9,28 +9,16 @@ import { CodeEditor } from './CodeEditor.tsx'
|
|
|
9
9
|
|
|
10
10
|
export interface FileViewerProps {
|
|
11
11
|
file: OpenFile | undefined
|
|
12
|
-
/** From `/fs/roots`. False renders the editor read-only rather than letting
|
|
13
|
-
* someone type into a file this gateway will refuse to write. */
|
|
14
12
|
canWrite?: boolean
|
|
15
13
|
onChange?: (path: string, content: string) => void
|
|
16
14
|
onSave?: (path: string) => void
|
|
17
|
-
/** Discard this tab's edits — local only, no re-read. */
|
|
18
15
|
onRevert?: (path: string) => void
|
|
19
|
-
/** Take the version on disk, discarding this tab's edits. */
|
|
20
16
|
onReload?: (path: string) => void
|
|
21
|
-
/** Take this tab's version, over whatever is on disk now. */
|
|
22
17
|
onOverwrite?: (path: string) => void
|
|
23
18
|
onDismissConflict?: (path: string) => void
|
|
24
19
|
className?: string
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
/**
|
|
28
|
-
* The focused file: Monaco, plus the states a file can be in that are not
|
|
29
|
-
* "here is some text".
|
|
30
|
-
*
|
|
31
|
-
* No path row — the tab's tooltip carries the path and the size, and a line of
|
|
32
|
-
* monospace above every file is chrome that never earns its height.
|
|
33
|
-
*/
|
|
34
22
|
export function FileViewer({
|
|
35
23
|
file,
|
|
36
24
|
canWrite,
|
|
@@ -42,36 +30,31 @@ export function FileViewer({
|
|
|
42
30
|
onDismissConflict,
|
|
43
31
|
className,
|
|
44
32
|
}: FileViewerProps) {
|
|
45
|
-
if (!file)
|
|
33
|
+
if (!file) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
46
36
|
|
|
47
37
|
return (
|
|
48
|
-
<div
|
|
49
|
-
data-slot='file-viewer'
|
|
50
|
-
className={cn('flex min-h-0 min-w-0 flex-1 flex-col bg-bg', className)}>
|
|
51
|
-
{/* The one failure with a choice attached, so it gets a bar rather than a
|
|
52
|
-
toast: the agent rewrote this file while it was open, and which
|
|
53
|
-
version wins is not something to decide on the user's behalf. */}
|
|
38
|
+
<div data-slot="file-viewer" className={cn('flex min-h-0 min-w-0 flex-1 flex-col bg-bg', className)}>
|
|
54
39
|
{file.conflict ? (
|
|
55
|
-
<div className=
|
|
56
|
-
<TriangleAlert className=
|
|
57
|
-
<span className=
|
|
58
|
-
|
|
59
|
-
</span>
|
|
60
|
-
<Button variant='outline' size='xs' onClick={() => onReload?.(file.path)}>
|
|
40
|
+
<div className="flex shrink-0 flex-wrap items-center gap-2 border-b border-warning/40 bg-warning-bg px-3 py-2">
|
|
41
|
+
<TriangleAlert className="size-3.5 shrink-0 text-warning" />
|
|
42
|
+
<span className="min-w-0 flex-1 text-body-sm text-warning">This file changed on disk since you opened it.</span>
|
|
43
|
+
<Button variant="outline" size="xs" onClick={() => onReload?.(file.path)}>
|
|
61
44
|
Use the version on disk
|
|
62
45
|
</Button>
|
|
63
|
-
<Button variant=
|
|
46
|
+
<Button variant="outline" size="xs" onClick={() => onOverwrite?.(file.path)}>
|
|
64
47
|
Keep mine
|
|
65
48
|
</Button>
|
|
66
|
-
<Button variant=
|
|
49
|
+
<Button variant="ghost" size="xs" onClick={() => onDismissConflict?.(file.path)}>
|
|
67
50
|
Dismiss
|
|
68
51
|
</Button>
|
|
69
52
|
</div>
|
|
70
53
|
) : file.saveError ? (
|
|
71
|
-
<div className=
|
|
72
|
-
<TriangleAlert className=
|
|
73
|
-
<span className=
|
|
74
|
-
<Button variant=
|
|
54
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-danger/40 bg-danger-bg px-3 py-2">
|
|
55
|
+
<TriangleAlert className="size-3.5 shrink-0 text-danger" />
|
|
56
|
+
<span className="min-w-0 flex-1 text-body-sm text-danger">{file.saveError}</span>
|
|
57
|
+
<Button variant="ghost" size="xs" onClick={() => onDismissConflict?.(file.path)}>
|
|
75
58
|
Dismiss
|
|
76
59
|
</Button>
|
|
77
60
|
</div>
|
|
@@ -79,20 +62,18 @@ export function FileViewer({
|
|
|
79
62
|
|
|
80
63
|
{file.status === 'loading' ? (
|
|
81
64
|
<Centred>
|
|
82
|
-
<Spinner className=
|
|
65
|
+
<Spinner className="size-4 text-fg-4" />
|
|
83
66
|
</Centred>
|
|
84
67
|
) : file.status === 'error' ? (
|
|
85
68
|
<Centred>
|
|
86
|
-
<TriangleAlert className=
|
|
87
|
-
<p className=
|
|
69
|
+
<TriangleAlert className="size-4 text-danger" />
|
|
70
|
+
<p className="text-body-sm text-danger">{file.error}</p>
|
|
88
71
|
</Centred>
|
|
89
72
|
) : file.status === 'binary' ? (
|
|
90
73
|
<Centred>
|
|
91
|
-
<FileWarning className=
|
|
92
|
-
<p className=
|
|
93
|
-
|
|
94
|
-
destroy it" are different reassurances. */}
|
|
95
|
-
<p className='text-label text-fg-4'>It can’t be edited here without corrupting it.</p>
|
|
74
|
+
<FileWarning className="size-4 text-fg-4" />
|
|
75
|
+
<p className="text-body-sm text-fg-4">This file isn’t text.</p>
|
|
76
|
+
<p className="text-label text-fg-4">It can’t be edited here without corrupting it.</p>
|
|
96
77
|
</Centred>
|
|
97
78
|
) : (
|
|
98
79
|
<CodeEditor
|
|
@@ -104,32 +85,25 @@ export function FileViewer({
|
|
|
104
85
|
/>
|
|
105
86
|
)}
|
|
106
87
|
|
|
107
|
-
{/* A status strip only while there is something to say. Saving is fast
|
|
108
|
-
enough that a permanent row would mostly be blank. */}
|
|
109
88
|
{file.status === 'ready' && (file.saving || isDirty(file) || !canWrite) ? (
|
|
110
|
-
<div className=
|
|
89
|
+
<div className="flex shrink-0 items-center gap-2 border-t border-border px-3 py-1">
|
|
111
90
|
{file.saving ? (
|
|
112
91
|
<>
|
|
113
|
-
<Spinner className=
|
|
114
|
-
<span className=
|
|
92
|
+
<Spinner className="size-3 text-fg-4" />
|
|
93
|
+
<span className="text-label text-fg-4">Saving…</span>
|
|
115
94
|
</>
|
|
116
95
|
) : !canWrite ? (
|
|
117
|
-
<span className=
|
|
118
|
-
Read-only — this gateway doesn’t allow writes.
|
|
119
|
-
</span>
|
|
96
|
+
<span className="text-label text-fg-4">Read-only — this gateway doesn’t allow writes.</span>
|
|
120
97
|
) : (
|
|
121
98
|
<>
|
|
122
|
-
<span className=
|
|
123
|
-
<span className=
|
|
124
|
-
|
|
125
|
-
this tab read. Re-reading is the conflict bar's job, and it is
|
|
126
|
-
a different question. */}
|
|
127
|
-
<Button variant='ghost' size='xs' onClick={() => onRevert?.(file.path)}>
|
|
99
|
+
<span className="text-label text-fg-3">Unsaved changes</span>
|
|
100
|
+
<span className="flex-1" />
|
|
101
|
+
<Button variant="ghost" size="xs" onClick={() => onRevert?.(file.path)}>
|
|
128
102
|
Revert
|
|
129
103
|
</Button>
|
|
130
|
-
<Button variant=
|
|
104
|
+
<Button variant="outline" size="xs" onClick={() => onSave?.(file.path)}>
|
|
131
105
|
Save
|
|
132
|
-
<span className=
|
|
106
|
+
<span className="ml-1 text-fg-4">⌘S</span>
|
|
133
107
|
</Button>
|
|
134
108
|
</>
|
|
135
109
|
)}
|
|
@@ -140,9 +114,5 @@ export function FileViewer({
|
|
|
140
114
|
}
|
|
141
115
|
|
|
142
116
|
function Centred({ children }: { children: ReactNode }) {
|
|
143
|
-
return
|
|
144
|
-
<div className='flex min-h-0 flex-1 flex-col items-center justify-center gap-2 p-6'>
|
|
145
|
-
{children}
|
|
146
|
-
</div>
|
|
147
|
-
)
|
|
117
|
+
return <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-2 p-6">{children}</div>
|
|
148
118
|
}
|