@workerdeck/ui 0.13.0 → 0.16.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/README.md +72 -0
- package/build/{SessionPanel-CZMA44NM.d.mts → SessionPanel-B9CHoq8x.d.mts} +213 -27
- package/build/{SessionPanel-CKQa4i0Y.mjs → SessionPanel-DII9MmQ8.mjs} +5192 -2542
- package/build/SessionPanel-DII9MmQ8.mjs.map +1 -0
- package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
- package/build/format.d.mts +39 -4
- package/build/format.mjs +2 -118
- package/build/index.d.mts +494 -45
- package/build/index.mjs +182 -24
- package/build/index.mjs.map +1 -1
- package/build/status-Ydzi7n6j.mjs +143 -0
- package/build/status-Ydzi7n6j.mjs.map +1 -0
- package/build/workspace.d.mts +13 -1
- package/build/workspace.mjs +111 -5
- package/build/workspace.mjs.map +1 -1
- package/package.json +14 -7
- package/src/components/agent/Composer.tsx +251 -84
- package/src/components/agent/Conversation.tsx +12 -12
- package/src/components/agent/FileCard.tsx +0 -26
- package/src/components/agent/FileTree.tsx +9 -8
- package/src/components/agent/Loader.tsx +22 -72
- package/src/components/agent/Message.tsx +11 -43
- package/src/components/agent/PermissionPrompt.tsx +0 -92
- package/src/components/agent/QuestionPrompt.tsx +0 -122
- package/src/components/agent/Reasoning.tsx +5 -19
- package/src/components/agent/Response.tsx +1 -132
- package/src/components/agent/SessionBrowser.tsx +35 -25
- package/src/components/agent/SessionPanel.tsx +312 -63
- package/src/components/agent/SessionWorkspace.tsx +36 -0
- package/src/components/agent/StatusBar.tsx +70 -15
- package/src/components/agent/ToolCallCard.tsx +17 -111
- package/src/components/agent/Transcript.tsx +710 -203
- package/src/components/agent/UsageDialog.tsx +20 -106
- package/src/components/agent/UsageMeters.tsx +133 -0
- package/src/components/agent/pulse.tsx +3 -2
- package/src/components/agent/transcript-rows.ts +82 -0
- package/src/components/agent/transcript-variant.tsx +43 -51
- package/src/components/agent/use-height-epoch.ts +60 -0
- package/src/components/agent/use-path-links.ts +147 -0
- package/src/components/agent/use-transcript-jumps.ts +190 -0
- package/src/components/prompt-area/cursor-helpers.ts +65 -0
- package/src/components/prompt-area/use-prompt-area.ts +16 -10
- package/src/components/terminal/PermissionPrompt.tsx +119 -0
- package/src/components/terminal/QuestionPrompt.tsx +322 -0
- package/src/components/terminal/StatusLine.tsx +159 -0
- package/src/components/terminal/TerminalTranscript.tsx +147 -0
- package/src/components/terminal/affordances.tsx +118 -0
- package/src/components/terminal/diff.tsx +130 -0
- package/src/components/terminal/height.ts +727 -0
- package/src/components/terminal/items.tsx +449 -0
- package/src/components/terminal/markdown.tsx +191 -0
- package/src/components/terminal/press.tsx +120 -0
- package/src/components/terminal/prompt.tsx +343 -0
- package/src/components/terminal/result-preview.ts +72 -0
- package/src/components/terminal/row.tsx +132 -0
- package/src/components/terminal/scrubber.tsx +663 -0
- package/src/components/terminal/surface.tsx +80 -0
- package/src/components/terminal/tool-run.ts +91 -0
- package/src/components/ui/Badge.tsx +6 -1
- package/src/components/ui/Empty.tsx +56 -0
- package/src/components/ui/Splitter.tsx +14 -0
- package/src/index.ts +36 -0
- package/src/lib/status.ts +59 -3
- package/src/lib/tool-icon.ts +14 -0
- package/src/styles/terminal.css +1011 -0
- package/src/styles/theme.css +73 -0
- package/build/SessionPanel-CKQa4i0Y.mjs.map +0 -1
- package/build/format.mjs.map +0 -1
- package/src/components/agent/line-prompt.tsx +0 -249
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
//#region src/lib/status.ts
|
|
2
|
+
const STATUS_META = {
|
|
3
|
+
starting: {
|
|
4
|
+
icon: "loading~spin",
|
|
5
|
+
label: "Starting",
|
|
6
|
+
severity: "none"
|
|
7
|
+
},
|
|
8
|
+
running: {
|
|
9
|
+
icon: "loading~spin",
|
|
10
|
+
label: "Running",
|
|
11
|
+
severity: "none"
|
|
12
|
+
},
|
|
13
|
+
awaiting_approval: {
|
|
14
|
+
icon: "warning",
|
|
15
|
+
label: "Needs approval",
|
|
16
|
+
severity: "warning"
|
|
17
|
+
},
|
|
18
|
+
idle: {
|
|
19
|
+
icon: "check",
|
|
20
|
+
label: "Idle",
|
|
21
|
+
severity: "none"
|
|
22
|
+
},
|
|
23
|
+
parked: {
|
|
24
|
+
icon: "debug-pause",
|
|
25
|
+
label: "Parked",
|
|
26
|
+
severity: "none"
|
|
27
|
+
},
|
|
28
|
+
failed: {
|
|
29
|
+
icon: "error",
|
|
30
|
+
label: "Failed",
|
|
31
|
+
severity: "error"
|
|
32
|
+
},
|
|
33
|
+
closed: {
|
|
34
|
+
icon: "circle-slash",
|
|
35
|
+
label: "Closed",
|
|
36
|
+
severity: "none"
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The status slot, connection first. A session status held over a dead socket is
|
|
41
|
+
* the last thing we heard, not the current state — so a lost link takes the slot
|
|
42
|
+
* rather than letting "Running" imply a turn is still streaming.
|
|
43
|
+
*/
|
|
44
|
+
function statusPresentation(vitals) {
|
|
45
|
+
if (!vitals) return {
|
|
46
|
+
icon: "hubot",
|
|
47
|
+
label: "Connecting…",
|
|
48
|
+
severity: "none"
|
|
49
|
+
};
|
|
50
|
+
if (vitals.connection === "offline") return {
|
|
51
|
+
icon: "debug-disconnect",
|
|
52
|
+
label: "Offline",
|
|
53
|
+
severity: "error"
|
|
54
|
+
};
|
|
55
|
+
if (vitals.connection === "reconnecting") return {
|
|
56
|
+
icon: "sync~spin",
|
|
57
|
+
label: "Reconnecting…",
|
|
58
|
+
severity: "warning"
|
|
59
|
+
};
|
|
60
|
+
return STATUS_META[vitals.status] ?? {
|
|
61
|
+
icon: "hubot",
|
|
62
|
+
label: vitals.status,
|
|
63
|
+
severity: "none"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** 0–100 → the colour a meter wears. One pair of thresholds for every surface. */
|
|
67
|
+
function meterSeverity(pct) {
|
|
68
|
+
if (pct === void 0) return "none";
|
|
69
|
+
if (pct >= 95) return "error";
|
|
70
|
+
if (pct >= 80) return "warning";
|
|
71
|
+
return "none";
|
|
72
|
+
}
|
|
73
|
+
/** The window a lane points at, or `undefined` when this account has none. */
|
|
74
|
+
function usageWindow(rateLimits, lane) {
|
|
75
|
+
if (!rateLimits) return void 0;
|
|
76
|
+
if (lane === "session") {
|
|
77
|
+
const info = rateLimits.five_hour;
|
|
78
|
+
return info ? {
|
|
79
|
+
key: "five_hour",
|
|
80
|
+
info
|
|
81
|
+
} : void 0;
|
|
82
|
+
}
|
|
83
|
+
if (lane === "weekly") {
|
|
84
|
+
const info = rateLimits.seven_day;
|
|
85
|
+
return info ? {
|
|
86
|
+
key: "seven_day",
|
|
87
|
+
info
|
|
88
|
+
} : void 0;
|
|
89
|
+
}
|
|
90
|
+
return tightestWindow(Object.fromEntries(Object.entries(rateLimits).filter(([key]) => key.startsWith("seven_day_") && key !== "seven_day_oauth_apps")));
|
|
91
|
+
}
|
|
92
|
+
/** The rate-limit window that gets the one visible slot: whichever is fullest,
|
|
93
|
+
* since the binding constraint is the one worth glancing at. Still the right
|
|
94
|
+
* rule for a surface with exactly one slot; {@link usageWindow} is for one with
|
|
95
|
+
* three. */
|
|
96
|
+
function tightestWindow(rateLimits) {
|
|
97
|
+
const entries = Object.entries(rateLimits ?? {});
|
|
98
|
+
if (entries.length === 0) return void 0;
|
|
99
|
+
let best;
|
|
100
|
+
for (const [key, info] of entries) if ((info.status === "rejected" ? Number.POSITIVE_INFINITY : info.utilization ?? -1) > (best === void 0 ? Number.NEGATIVE_INFINITY : best.info.status === "rejected" ? Number.POSITIVE_INFINITY : best.info.utilization ?? -1)) best = {
|
|
101
|
+
key,
|
|
102
|
+
info
|
|
103
|
+
};
|
|
104
|
+
return best;
|
|
105
|
+
}
|
|
106
|
+
/** A rate-limit window's key, named for a human. A model-scoped bucket is named
|
|
107
|
+
* for its model alone (`seven_day_fable` → "Fable"): the lane it sits in
|
|
108
|
+
* already says weekly, and "Seven day fable" in a status bar is three words to
|
|
109
|
+
* say one. */
|
|
110
|
+
function windowLabel(key) {
|
|
111
|
+
if (key === "five_hour") return "Session";
|
|
112
|
+
if (key === "seven_day") return "Weekly";
|
|
113
|
+
const words = (key.startsWith("seven_day_") ? key.slice(10) : key).replaceAll("_", " ");
|
|
114
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The catalog row a session is actually running, or `undefined` for a model the
|
|
118
|
+
* list doesn't name. Matched leniently: a session reports the *resolved* id
|
|
119
|
+
* (`claude-sonnet-5`) where the row may be keyed on the alias (`sonnet`), and
|
|
120
|
+
* either can carry a `[1m]` context-window suffix.
|
|
121
|
+
*/
|
|
122
|
+
function currentModel(vitals) {
|
|
123
|
+
const id = vitals?.model;
|
|
124
|
+
if (!id) return void 0;
|
|
125
|
+
const bare = (value) => value.replace(/\[.*\]$/, "");
|
|
126
|
+
const wanted = bare(id);
|
|
127
|
+
return vitals.models.find((m) => bare(m.value) === wanted || m.resolvedModel && bare(m.resolvedModel) === wanted);
|
|
128
|
+
}
|
|
129
|
+
/** A session's model, named the way the picker names it. Falls back to the raw
|
|
130
|
+
* id, and to "Default" while the session is on the CLI's own pick. */
|
|
131
|
+
function modelLabel(vitals) {
|
|
132
|
+
if (!vitals?.model) return "Default";
|
|
133
|
+
return currentModel(vitals)?.displayName ?? vitals.model;
|
|
134
|
+
}
|
|
135
|
+
/** Context percentage as its meter severity — the reading and the colour come
|
|
136
|
+
* from one place so a panel and a status bar never disagree. */
|
|
137
|
+
function contextSeverity(usage) {
|
|
138
|
+
return meterSeverity(usage?.percentage);
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { statusPresentation as a, windowLabel as c, modelLabel as i, currentModel as n, tightestWindow as o, meterSeverity as r, usageWindow as s, contextSeverity as t };
|
|
142
|
+
|
|
143
|
+
//# sourceMappingURL=status-Ydzi7n6j.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-Ydzi7n6j.mjs","names":[],"sources":["../src/lib/status.ts"],"sourcesContent":["import type { ContextUsage, ModelOption, RateLimitInfo, SessionStatus } from '@workerdeck/protocol'\n\n/**\n * How a session's live readings become a status line — the pure half, so every\n * host spells \"Needs approval\", \"80% is a warning\" and \"which window is the\n * binding one\" the same way.\n *\n * Structurally typed against `SessionVitals` rather than importing it: this file\n * ships from the React-free `@workerdeck/ui/format` entry, and a host drawing\n * the readings outside React (the VS Code extension host in the window status\n * bar) must not pull a component graph in to do it. A real `SessionVitals`\n * satisfies every shape here.\n */\nexport type StatusSeverity = 'none' | 'warning' | 'error'\n\n/** What a status slot shows, before any host's icon vocabulary gets involved.\n * `icon` is a VS Code codicon name — the one host-shaped thing left, because the\n * alternative is a second mapping table in the only consumer. */\nexport type StatusPresentation = {\n icon: string\n label: string\n severity: StatusSeverity\n}\n\nexport type StatusReadings = {\n status: SessionStatus\n /** `@workerdeck/react`'s `ConnectionState`, structurally — the link state wins\n * the slot, so it has to be part of the reading. */\n connection?: 'live' | 'reconnecting' | 'offline'\n}\n\nconst STATUS_META: Record<SessionStatus, StatusPresentation> = {\n starting: { icon: 'loading~spin', label: 'Starting', severity: 'none' },\n running: { icon: 'loading~spin', label: 'Running', severity: 'none' },\n awaiting_approval: { icon: 'warning', label: 'Needs approval', severity: 'warning' },\n idle: { icon: 'check', label: 'Idle', severity: 'none' },\n parked: { icon: 'debug-pause', label: 'Parked', severity: 'none' },\n failed: { icon: 'error', label: 'Failed', severity: 'error' },\n closed: { icon: 'circle-slash', label: 'Closed', severity: 'none' },\n}\n\n/**\n * The status slot, connection first. A session status held over a dead socket is\n * the last thing we heard, not the current state — so a lost link takes the slot\n * rather than letting \"Running\" imply a turn is still streaming.\n */\nexport function statusPresentation(vitals: StatusReadings | undefined): StatusPresentation {\n if (!vitals) return { icon: 'hubot', label: 'Connecting…', severity: 'none' }\n if (vitals.connection === 'offline') {\n return { icon: 'debug-disconnect', label: 'Offline', severity: 'error' }\n }\n if (vitals.connection === 'reconnecting') {\n return { icon: 'sync~spin', label: 'Reconnecting…', severity: 'warning' }\n }\n return STATUS_META[vitals.status] ?? { icon: 'hubot', label: vitals.status, severity: 'none' }\n}\n\n/** 0–100 → the colour a meter wears. One pair of thresholds for every surface. */\nexport function meterSeverity(pct: number | undefined): StatusSeverity {\n if (pct === undefined) return 'none'\n if (pct >= 95) return 'error'\n if (pct >= 80) return 'warning'\n return 'none'\n}\n\n/**\n * The three *lanes* a plan-usage reading can occupy, and the whole reason this\n * is a rule rather than a per-client `if`.\n *\n * The CLI reports one window per limit (`five_hour`, `seven_day`, and a\n * `seven_day_<model>` bucket per model-scoped limit — `seven_day_opus`,\n * `seven_day_sonnet`, and whatever `model_scoped` names next). A surface with\n * one slot shows the fullest of them, which is why `tightestWindow` exists —\n * but that answers \"what is closest to blocking me\", not \"how much of *this\n * session's* budget have I spent\", and those are different questions a reader\n * asks at different times. A weekly window at 71% will win the single slot over\n * a five-hour window at 60% every time, so the reading you actually watch while\n * working is the one you can never see.\n *\n * Hence three lanes, each independently showable:\n *\n * - `'session'` — the five-hour window. The one that resets while you work.\n * - `'weekly'` — the plain seven-day window, the account-wide ceiling.\n * - `'model'` — the fullest of the *model-scoped* weekly buckets. Deliberately\n * not a named model: which models have their own bucket is the plan's\n * business and changes without notice, so a client that hardcoded\n * `seven_day_opus` would show nothing the month it becomes something else.\n * The label comes from the key, so this lane names whatever it found.\n */\nexport type UsageLane = 'session' | 'weekly' | 'model'\n\n/** The window a lane points at, or `undefined` when this account has none. */\nexport function usageWindow(\n rateLimits: Record<string, RateLimitInfo> | undefined,\n lane: UsageLane,\n): { key: string; info: RateLimitInfo } | undefined {\n if (!rateLimits) return undefined\n if (lane === 'session') {\n const info = rateLimits.five_hour\n return info ? { key: 'five_hour', info } : undefined\n }\n if (lane === 'weekly') {\n const info = rateLimits.seven_day\n return info ? { key: 'seven_day', info } : undefined\n }\n // Model-scoped: same \"fullest wins\" rule as the single slot, over the subset.\n const scoped = Object.fromEntries(\n Object.entries(rateLimits).filter(\n ([key]) => key.startsWith('seven_day_') && key !== 'seven_day_oauth_apps',\n ),\n )\n return tightestWindow(scoped)\n}\n\n/** The rate-limit window that gets the one visible slot: whichever is fullest,\n * since the binding constraint is the one worth glancing at. Still the right\n * rule for a surface with exactly one slot; {@link usageWindow} is for one with\n * three. */\nexport function tightestWindow(\n rateLimits: Record<string, RateLimitInfo> | undefined,\n): { key: string; info: RateLimitInfo } | undefined {\n const entries = Object.entries(rateLimits ?? {})\n if (entries.length === 0) return undefined\n let best: { key: string; info: RateLimitInfo } | undefined\n for (const [key, info] of entries) {\n // A rejected window outranks any utilization: it is the one actually blocking.\n const rank = info.status === 'rejected' ? Number.POSITIVE_INFINITY : (info.utilization ?? -1)\n const bestRank =\n best === undefined\n ? Number.NEGATIVE_INFINITY\n : best.info.status === 'rejected'\n ? Number.POSITIVE_INFINITY\n : (best.info.utilization ?? -1)\n if (rank > bestRank) best = { key, info }\n }\n return best\n}\n\n/** A rate-limit window's key, named for a human. A model-scoped bucket is named\n * for its model alone (`seven_day_fable` → \"Fable\"): the lane it sits in\n * already says weekly, and \"Seven day fable\" in a status bar is three words to\n * say one. */\nexport function windowLabel(key: string): string {\n if (key === 'five_hour') return 'Session'\n if (key === 'seven_day') return 'Weekly'\n const scoped = key.startsWith('seven_day_') ? key.slice('seven_day_'.length) : key\n const words = scoped.replaceAll('_', ' ')\n return words.charAt(0).toUpperCase() + words.slice(1)\n}\n\nexport type ModelReadings = { model?: string; models: readonly ModelOption[] }\n\n/**\n * The catalog row a session is actually running, or `undefined` for a model the\n * list doesn't name. Matched leniently: a session reports the *resolved* id\n * (`claude-sonnet-5`) where the row may be keyed on the alias (`sonnet`), and\n * either can carry a `[1m]` context-window suffix.\n */\nexport function currentModel(vitals: ModelReadings | undefined): ModelOption | undefined {\n const id = vitals?.model\n if (!id) return undefined\n const bare = (value: string) => value.replace(/\\[.*\\]$/, '')\n const wanted = bare(id)\n return vitals.models.find(\n (m) => bare(m.value) === wanted || (m.resolvedModel && bare(m.resolvedModel) === wanted),\n )\n}\n\n/** A session's model, named the way the picker names it. Falls back to the raw\n * id, and to \"Default\" while the session is on the CLI's own pick. */\nexport function modelLabel(vitals: ModelReadings | undefined): string {\n if (!vitals?.model) return 'Default'\n return currentModel(vitals)?.displayName ?? vitals.model\n}\n\n/** Context percentage as its meter severity — the reading and the colour come\n * from one place so a panel and a status bar never disagree. */\nexport function contextSeverity(usage: ContextUsage | undefined): StatusSeverity {\n return meterSeverity(usage?.percentage)\n}\n"],"mappings":"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"}
|
package/build/workspace.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as SessionPanelProps } from "./SessionPanel-
|
|
1
|
+
import { r as SessionPanelProps } from "./SessionPanel-B9CHoq8x.mjs";
|
|
2
2
|
import * as _$react from "react";
|
|
3
3
|
import { CSSProperties } from "react";
|
|
4
4
|
import { OpenFile, UseHostFileSearchResult, UseHostFileTreeResult } from "@workerdeck/react";
|
|
@@ -21,8 +21,15 @@ interface SessionWorkspaceProps {
|
|
|
21
21
|
*/
|
|
22
22
|
transcriptVariant?: SessionPanelProps['transcriptVariant'];
|
|
23
23
|
transcriptDensity?: SessionPanelProps['transcriptDensity'];
|
|
24
|
+
transcriptFont?: SessionPanelProps['transcriptFont'];
|
|
25
|
+
/** The overview ruler in place of the scrollbar — see `SessionPanel`. */
|
|
26
|
+
scrubber?: SessionPanelProps['scrubber'];
|
|
27
|
+
scrubberMarks?: SessionPanelProps['scrubberMarks'];
|
|
28
|
+
/** The last prompt pinned above the transcript — see `SessionPanel`. */
|
|
29
|
+
stickyPrompt?: SessionPanelProps['stickyPrompt'];
|
|
24
30
|
/** Which end of the panel the status bar sits at — see `SessionPanel`. */
|
|
25
31
|
statusPlacement?: SessionPanelProps['statusPlacement'];
|
|
32
|
+
controlsSurface?: SessionPanelProps['controlsSurface'];
|
|
26
33
|
unseen?: SessionPanelProps['unseen'];
|
|
27
34
|
/** Viewer mode — no composer, no approval prompts. Forwarded verbatim to
|
|
28
35
|
* {@link SessionPanel}; the file tree and editor are unaffected, since reading
|
|
@@ -72,7 +79,12 @@ declare function SessionWorkspace({
|
|
|
72
79
|
header,
|
|
73
80
|
transcriptVariant,
|
|
74
81
|
transcriptDensity,
|
|
82
|
+
transcriptFont,
|
|
83
|
+
scrubber,
|
|
84
|
+
scrubberMarks,
|
|
85
|
+
stickyPrompt,
|
|
75
86
|
statusPlacement,
|
|
87
|
+
controlsSurface,
|
|
76
88
|
unseen,
|
|
77
89
|
readOnly,
|
|
78
90
|
onVitals,
|
package/build/workspace.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Bt as Button, Ht as cn, Lt as Input, dt as Splitter, gt as Tip, pt as Spinner, t as SessionPanel, vt as TooltipProvider } from "./SessionPanel-DII9MmQ8.mjs";
|
|
2
2
|
import { n as formatBytes } from "./format-DqR56Y8l.mjs";
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -180,7 +180,7 @@ function FileTree({ tree, search, activePath, onOpenFile, onCollapse, style, cla
|
|
|
180
180
|
}) : null
|
|
181
181
|
]
|
|
182
182
|
}) : /* @__PURE__ */ jsx("span", {
|
|
183
|
-
className: "min-w-0 flex-1 truncate px-1
|
|
183
|
+
className: "min-w-0 flex-1 truncate px-1 text-label text-fg-4",
|
|
184
184
|
children: tree.root
|
|
185
185
|
}),
|
|
186
186
|
/* @__PURE__ */ jsx(Button, {
|
|
@@ -267,11 +267,11 @@ function Row({ label, detail, title, icon, chevron, indent = 0, active, onClick
|
|
|
267
267
|
}),
|
|
268
268
|
icon,
|
|
269
269
|
/* @__PURE__ */ jsx("span", {
|
|
270
|
-
className: "shrink-0 truncate
|
|
270
|
+
className: "shrink-0 truncate text-label",
|
|
271
271
|
children: label
|
|
272
272
|
}),
|
|
273
273
|
detail ? /* @__PURE__ */ jsx("span", {
|
|
274
|
-
className: "min-w-0 flex-1 truncate
|
|
274
|
+
className: "min-w-0 flex-1 truncate text-label text-fg-4",
|
|
275
275
|
children: detail
|
|
276
276
|
}) : null
|
|
277
277
|
]
|
|
@@ -665,6 +665,100 @@ function Centred({ children }) {
|
|
|
665
665
|
});
|
|
666
666
|
}
|
|
667
667
|
//#endregion
|
|
668
|
+
//#region src/components/agent/use-path-links.ts
|
|
669
|
+
/** Marks the element under the pointer while the modifier is held. */
|
|
670
|
+
const LINKISH = "wd-path-link";
|
|
671
|
+
/** Absolute, or relative with at least one separator. `:12` is a line number. */
|
|
672
|
+
const PATH_RE = /^(\/[^\s:'"`()[\]{}]+|[\w.@~-]+(?:\/[\w.@~-]+)+)(?::(\d+))?$/;
|
|
673
|
+
/** A bare filename — only trusted inside a code element. */
|
|
674
|
+
const FILE_RE = /^([\w.@-]+\.[A-Za-z0-9]{1,10})(?::(\d+))?$/;
|
|
675
|
+
/** How much of an element's text the match must cover, outside code. */
|
|
676
|
+
const COVERAGE = .6;
|
|
677
|
+
function matchPath(text, inCode) {
|
|
678
|
+
const trimmed = text.trim();
|
|
679
|
+
if (!trimmed || trimmed.endsWith("/")) return void 0;
|
|
680
|
+
const hit = PATH_RE.exec(trimmed) ?? (inCode ? FILE_RE.exec(trimmed) : null);
|
|
681
|
+
if (!hit) return void 0;
|
|
682
|
+
return {
|
|
683
|
+
path: hit[1],
|
|
684
|
+
line: hit[2] ? Number(hit[2]) : void 0
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/** The path a click on this element means, if any. */
|
|
688
|
+
function hitFor(element) {
|
|
689
|
+
if (!element) return void 0;
|
|
690
|
+
const inCode = element.closest("code") !== null;
|
|
691
|
+
const text = element.textContent ?? "";
|
|
692
|
+
const hit = matchPath(text, inCode);
|
|
693
|
+
if (!hit) return void 0;
|
|
694
|
+
if (!inCode && hit.path.length < text.trim().length * COVERAGE) return void 0;
|
|
695
|
+
return hit;
|
|
696
|
+
}
|
|
697
|
+
function usePathLinks({ container, onOpen, enabled = true, ignore }) {
|
|
698
|
+
useEffect(() => {
|
|
699
|
+
const root = container.current;
|
|
700
|
+
if (!root || !enabled) return;
|
|
701
|
+
const excluded = (element) => ignore !== void 0 && element?.closest(ignore) !== null && element !== void 0;
|
|
702
|
+
let marked;
|
|
703
|
+
const unmark = () => {
|
|
704
|
+
marked?.classList.remove(LINKISH);
|
|
705
|
+
marked = void 0;
|
|
706
|
+
};
|
|
707
|
+
const onClick = (event) => {
|
|
708
|
+
if (!event.metaKey && !event.ctrlKey) return;
|
|
709
|
+
const target = event.target instanceof HTMLElement ? event.target : void 0;
|
|
710
|
+
if (excluded(target)) return;
|
|
711
|
+
const hit = hitFor(target);
|
|
712
|
+
if (!hit) return;
|
|
713
|
+
event.preventDefault();
|
|
714
|
+
event.stopPropagation();
|
|
715
|
+
unmark();
|
|
716
|
+
onOpen(hit);
|
|
717
|
+
};
|
|
718
|
+
const onMove = (event) => {
|
|
719
|
+
if (!event.metaKey && !event.ctrlKey) return unmark();
|
|
720
|
+
const element = event.target instanceof HTMLElement ? event.target : void 0;
|
|
721
|
+
if (element === marked) return;
|
|
722
|
+
unmark();
|
|
723
|
+
if (!element || excluded(element) || !hitFor(element)) return;
|
|
724
|
+
marked = element;
|
|
725
|
+
element.classList.add(LINKISH);
|
|
726
|
+
};
|
|
727
|
+
const onKey = (event) => {
|
|
728
|
+
if (!event.metaKey && !event.ctrlKey) unmark();
|
|
729
|
+
};
|
|
730
|
+
root.addEventListener("click", onClick, true);
|
|
731
|
+
root.addEventListener("mousemove", onMove);
|
|
732
|
+
document.addEventListener("keydown", onKey);
|
|
733
|
+
document.addEventListener("keyup", onKey);
|
|
734
|
+
window.addEventListener("blur", unmark);
|
|
735
|
+
return () => {
|
|
736
|
+
unmark();
|
|
737
|
+
root.removeEventListener("click", onClick, true);
|
|
738
|
+
root.removeEventListener("mousemove", onMove);
|
|
739
|
+
document.removeEventListener("keydown", onKey);
|
|
740
|
+
document.removeEventListener("keyup", onKey);
|
|
741
|
+
window.removeEventListener("blur", unmark);
|
|
742
|
+
};
|
|
743
|
+
}, [
|
|
744
|
+
container,
|
|
745
|
+
onOpen,
|
|
746
|
+
enabled,
|
|
747
|
+
ignore
|
|
748
|
+
]);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* A transcript path against the session's cwd.
|
|
752
|
+
*
|
|
753
|
+
* Absolute wins outright. Everything else is relative to where the agent is
|
|
754
|
+
* working, which is the only root that makes `worker.mjs` mean anything.
|
|
755
|
+
*/
|
|
756
|
+
function resolveAgainstCwd(path, cwd) {
|
|
757
|
+
if (path.startsWith("/")) return path;
|
|
758
|
+
if (!cwd) return path;
|
|
759
|
+
return `${cwd.replace(/\/$/, "")}/${path.replace(/^\.\//, "")}`;
|
|
760
|
+
}
|
|
761
|
+
//#endregion
|
|
668
762
|
//#region src/components/agent/SessionWorkspace.tsx
|
|
669
763
|
const RAIL_MIN = 180;
|
|
670
764
|
const RAIL_MAX = 520;
|
|
@@ -694,7 +788,7 @@ const EDITOR_MIN = 120;
|
|
|
694
788
|
* The conditional children before it are `? :` expressions that leave a null in
|
|
695
789
|
* their slot, which is exactly what keeps its index stable.
|
|
696
790
|
*/
|
|
697
|
-
function SessionWorkspace({ client, sessionId, header, transcriptVariant, transcriptDensity, statusPlacement, unseen, readOnly, onVitals, defaultRailWidth = 260, defaultRailCollapsed, onRailChange, className }) {
|
|
791
|
+
function SessionWorkspace({ client, sessionId, header, transcriptVariant, transcriptDensity, transcriptFont, scrubber, scrubberMarks, stickyPrompt, statusPlacement, controlsSurface, unseen, readOnly, onVitals, defaultRailWidth = 260, defaultRailCollapsed, onRailChange, className }) {
|
|
698
792
|
const { info } = useSessionInfo(client, sessionId);
|
|
699
793
|
const cwd = info?.cwd;
|
|
700
794
|
const tree = useHostFileTree(client, cwd);
|
|
@@ -735,6 +829,12 @@ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transc
|
|
|
735
829
|
}, [files]);
|
|
736
830
|
const column = useRef(null);
|
|
737
831
|
const columnHeight = useElementHeight(column);
|
|
832
|
+
usePathLinks({
|
|
833
|
+
container: column,
|
|
834
|
+
onOpen: useCallback(({ path }) => files.open(resolveAgainstCwd(path, cwd)), [files.open, cwd]),
|
|
835
|
+
enabled: tree.available,
|
|
836
|
+
ignore: ".monaco-editor"
|
|
837
|
+
});
|
|
738
838
|
const editorMax = Math.max(EDITOR_MIN, columnHeight - AGENT_MIN);
|
|
739
839
|
const [topBar, setTopBar] = useState(null);
|
|
740
840
|
const hoisted = header === void 0 || topBar === null ? void 0 : typeof header === "function" ? (slots) => createPortal(header(slots), topBar) : createPortal(header, topBar);
|
|
@@ -771,6 +871,7 @@ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transc
|
|
|
771
871
|
onValueChange: setRailWidth,
|
|
772
872
|
min: RAIL_MIN,
|
|
773
873
|
max: RAIL_MAX,
|
|
874
|
+
defaultValue: defaultRailWidth,
|
|
774
875
|
"aria-label": "Resize the file tree"
|
|
775
876
|
}) : null,
|
|
776
877
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -810,6 +911,11 @@ function SessionWorkspace({ client, sessionId, header, transcriptVariant, transc
|
|
|
810
911
|
header: hoisted,
|
|
811
912
|
transcriptVariant,
|
|
812
913
|
transcriptDensity,
|
|
914
|
+
transcriptFont,
|
|
915
|
+
scrubber,
|
|
916
|
+
scrubberMarks,
|
|
917
|
+
stickyPrompt,
|
|
918
|
+
controlsSurface,
|
|
813
919
|
statusPlacement,
|
|
814
920
|
unseen,
|
|
815
921
|
readOnly,
|