@workerdeck/ui 0.7.0 → 0.11.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.
Files changed (61) hide show
  1. package/README.md +81 -3
  2. package/build/SessionPanel-CyhygZx_.d.mts +277 -0
  3. package/build/SessionPanel-_U8tjX29.mjs +8409 -0
  4. package/build/SessionPanel-_U8tjX29.mjs.map +1 -0
  5. package/build/format-DqR56Y8l.mjs +162 -0
  6. package/build/format-DqR56Y8l.mjs.map +1 -0
  7. package/build/format-ljc3lKpA.d.mts +59 -0
  8. package/build/format.d.mts +2 -0
  9. package/build/format.mjs +2 -0
  10. package/build/index.d.mts +615 -87
  11. package/build/index.mjs +6 -5081
  12. package/build/index.mjs.map +1 -1
  13. package/build/workspace.d.mts +199 -0
  14. package/build/workspace.mjs +849 -0
  15. package/build/workspace.mjs.map +1 -0
  16. package/package.json +22 -4
  17. package/src/components/agent/CodeEditor.tsx +300 -0
  18. package/src/components/agent/Composer.tsx +522 -87
  19. package/src/components/agent/ContextDialog.tsx +99 -0
  20. package/src/components/agent/Conversation.tsx +11 -3
  21. package/src/components/agent/EditorTabs.tsx +165 -0
  22. package/src/components/agent/FileCard.tsx +26 -0
  23. package/src/components/agent/FileTree.tsx +287 -0
  24. package/src/components/agent/FileViewer.tsx +148 -0
  25. package/src/components/agent/HostFilesDialog.tsx +218 -0
  26. package/src/components/agent/Loader.tsx +120 -14
  27. package/src/components/agent/McpDialog.tsx +363 -0
  28. package/src/components/agent/Message.tsx +51 -17
  29. package/src/components/agent/ModelSelect.tsx +34 -6
  30. package/src/components/agent/PermissionModeSelect.tsx +133 -22
  31. package/src/components/agent/PermissionPrompt.tsx +164 -6
  32. package/src/components/agent/PromptTokenText.tsx +39 -0
  33. package/src/components/agent/QuestionPrompt.tsx +122 -0
  34. package/src/components/agent/Reasoning.tsx +20 -5
  35. package/src/components/agent/Response.tsx +128 -0
  36. package/src/components/agent/SessionEmptyState.tsx +65 -0
  37. package/src/components/agent/SessionInfoDialog.tsx +163 -0
  38. package/src/components/agent/SessionPanel.tsx +756 -90
  39. package/src/components/agent/SessionWorkspace.tsx +282 -0
  40. package/src/components/agent/SkillsDialog.tsx +195 -0
  41. package/src/components/agent/StatusBar.tsx +85 -18
  42. package/src/components/agent/ToolCallCard.tsx +243 -30
  43. package/src/components/agent/Transcript.tsx +540 -27
  44. package/src/components/agent/UsageDialog.tsx +168 -0
  45. package/src/components/agent/line-prompt.tsx +249 -0
  46. package/src/components/agent/transcript-variant.tsx +61 -0
  47. package/src/components/prompt-area/prompt-area-engine.ts +53 -0
  48. package/src/components/prompt-area/types.ts +15 -0
  49. package/src/components/prompt-area/use-prompt-area.ts +20 -0
  50. package/src/components/ui/CodeBlock.tsx +40 -2
  51. package/src/components/ui/CopyButton.tsx +28 -3
  52. package/src/components/ui/Dialog.tsx +92 -0
  53. package/src/components/ui/Menu.tsx +55 -0
  54. package/src/components/ui/Splitter.tsx +133 -0
  55. package/src/components/ui/Tooltip.tsx +22 -5
  56. package/src/format.ts +10 -0
  57. package/src/index.ts +63 -2
  58. package/src/lib/clipboard.ts +56 -0
  59. package/src/lib/format.ts +114 -0
  60. package/src/lib/tool-icon.ts +96 -0
  61. package/src/workspace.ts +28 -0
package/src/lib/format.ts CHANGED
@@ -51,6 +51,54 @@ export function formatRelativeTime(epochMs: number | undefined, now = Date.now()
51
51
  return `${d}d ago`
52
52
  }
53
53
 
54
+ /**
55
+ * Human label for a rate-limit window key, compact: 'five_hour' → "5h",
56
+ * 'seven_day_opus' → "7d opus". The per-model suffix is an open set — the CLI
57
+ * adds buckets as plans gain them — so it is rewritten rather than enumerated.
58
+ */
59
+ export function formatRateLimitWindow(key: string): string {
60
+ if (key === 'five_hour') return '5h'
61
+ if (key === 'seven_day') return '7d'
62
+ const spaced = key.replaceAll('_', ' ')
63
+ return key.startsWith('seven_day_') ? `7d ${spaced.slice('seven day '.length)}` : spaced
64
+ }
65
+
66
+ /** The same key spelled out, where there is room: 'five_hour' → "5-hour
67
+ * session", 'seven_day_fable' → "Weekly · Fable". */
68
+ export function formatRateLimitWindowLong(key: string): string {
69
+ if (key === 'five_hour') return '5-hour session'
70
+ if (key === 'seven_day') return 'Weekly'
71
+ if (key === 'seven_day_oauth_apps') return 'Weekly · apps'
72
+ const capitalize = (s: string) => s.replace(/\b\w/g, (c) => c.toUpperCase())
73
+ if (!key.startsWith('seven_day_')) return capitalize(key.replaceAll('_', ' '))
74
+ return `Weekly · ${capitalize(key.slice('seven_day_'.length).replaceAll('_', ' '))}`
75
+ }
76
+
77
+ /**
78
+ * How long a rate-limit window is, in seconds — the denominator behind the pace
79
+ * marker. Derived from the key rather than reported: the CLI sends a reset time
80
+ * and a percentage, never a duration. `undefined` for a window whose key doesn't
81
+ * say, and the marker is then simply not drawn rather than guessed.
82
+ */
83
+ export function rateLimitWindowSeconds(key: string): number | undefined {
84
+ if (key === 'five_hour') return 5 * 3600
85
+ if (key.startsWith('seven_day')) return 7 * 86_400
86
+ return undefined
87
+ }
88
+
89
+ /** "8 secs ago" / "3 mins ago" — a freshness line finer-grained than
90
+ * {@link formatRelativeTime}, because a poll that just landed should say so. */
91
+ export function formatAgoPrecise(epochMs: number, now = Date.now()): string {
92
+ const seconds = Math.max(0, Math.floor((now - epochMs) / 1000))
93
+ if (seconds < 60) return `${seconds} sec${seconds === 1 ? '' : 's'} ago`
94
+ if (seconds < 3600) {
95
+ const minutes = Math.floor(seconds / 60)
96
+ return `${minutes} min${minutes === 1 ? '' : 's'} ago`
97
+ }
98
+ const hours = Math.floor(seconds / 3600)
99
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`
100
+ }
101
+
54
102
  /** Compact one-line preview of a tool input for card headers. */
55
103
  export function toolInputPreview(input: unknown, max = 80): string {
56
104
  if (input === null || input === undefined) return ''
@@ -65,3 +113,69 @@ export function toolInputPreview(input: unknown, max = 80): string {
65
113
  const text = JSON.stringify(input) ?? ''
66
114
  return text.length > max ? text.slice(0, max - 1) + '…' : text
67
115
  }
116
+
117
+ /** Families whose name isn't just a capitalised first letter, and how the
118
+ * vendor writes the version after it. GPT is `GPT-5.6`; everyone else spaces
119
+ * it. Anything unlisted is title-cased and spaced. */
120
+ const MODEL_FAMILIES: Record<string, { name: string; joiner?: string }> = {
121
+ gpt: { name: 'GPT', joiner: '-' },
122
+ deepseek: { name: 'DeepSeek' },
123
+ glm: { name: 'GLM' },
124
+ qwen: { name: 'Qwen' },
125
+ kimi: { name: 'Kimi' },
126
+ llama: { name: 'Llama' },
127
+ mistral: { name: 'Mistral' },
128
+ grok: { name: 'Grok' },
129
+ }
130
+
131
+ /**
132
+ * The name a person says, from a wire model id:
133
+ *
134
+ * - `claude-opus-5[1m]` → "Opus 5"
135
+ * - `claude-haiku-4-5-20251001` → "Haiku 4.5"
136
+ * - `gpt-5.6-luna` → "GPT-5.6 Luna"
137
+ * - `gemini-2.5-pro` → "Gemini 2.5 Pro"
138
+ * - `o3-mini` → "o3 Mini"
139
+ *
140
+ * Three kinds of token after the family, because vendors mix them freely: a
141
+ * **version** (`5`, `4-5`, `5.6` — joined with dots, since Anthropic splits what
142
+ * OpenAI writes as one token), a **code name or tier** (`luna`, `codex`, `pro`,
143
+ * `mini` — kept and capitalised, since it is often the only thing telling two
144
+ * models apart), and a **snapshot date** (`20251001` — dropped; it is a build,
145
+ * not a version).
146
+ *
147
+ * Anything genuinely unreadable falls back to the id: a wrong name is worse than
148
+ * a raw one, which is at least true.
149
+ *
150
+ * The server has a narrower version of this (`friendlyModelName` in core's
151
+ * `normalize.ts`) that derives Claude catalog names at authoring time. This one
152
+ * is the *render-time* fallback for an id with no catalog row behind it — the
153
+ * sidebar has only `SessionInfo.model` — so it has to cope with every vendor the
154
+ * provider engine can reach, not just the CLI's own.
155
+ */
156
+ export function friendlyModel(id: string | undefined): string | undefined {
157
+ if (!id) return undefined
158
+ const withoutVariant = id.split('[')[0] ?? id
159
+ const parts = withoutVariant.toLowerCase().split('-').filter(Boolean)
160
+ if (parts[0] === 'claude') parts.shift()
161
+ const familyToken = parts.shift()
162
+ if (!familyToken) return id
163
+ const family = MODEL_FAMILIES[familyToken]
164
+ const name =
165
+ family?.name ??
166
+ // OpenAI's reasoning series is lower-case by its own convention ('o3-mini'),
167
+ // and "O3" reads as a different product.
168
+ (/^o\d+$/.test(familyToken)
169
+ ? familyToken
170
+ : `${familyToken.charAt(0).toUpperCase()}${familyToken.slice(1)}`)
171
+
172
+ const version: string[] = []
173
+ const words: string[] = []
174
+ for (const part of parts) {
175
+ if (/^\d{8}$/.test(part)) continue
176
+ if (/^\d+(\.\d+)?$/.test(part)) version.push(part)
177
+ else words.push(`${part.charAt(0).toUpperCase()}${part.slice(1)}`)
178
+ }
179
+ const versioned = version.length > 0 ? `${name}${family?.joiner ?? ' '}${version.join('.')}` : name
180
+ return [versioned, ...words].join(' ')
181
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ ArrowDownCircle,
3
+ CheckSquare,
4
+ FileDiff,
5
+ FileText,
6
+ FolderSearch,
7
+ Globe,
8
+ Image,
9
+ type LucideIcon,
10
+ MessageCircleQuestion,
11
+ PencilLine,
12
+ Puzzle,
13
+ Search,
14
+ Sparkles,
15
+ SquarePen,
16
+ Terminal,
17
+ UsersRound,
18
+ Wrench,
19
+ } from 'lucide-react'
20
+
21
+ /**
22
+ * An icon per tool, so a transcript can be skimmed by shape rather than read.
23
+ *
24
+ * The same mapping the iOS app makes, in lucide's vocabulary rather than SF
25
+ * Symbols — the two clients should be recognisably showing the same thing. An
26
+ * unknown tool falls back to a wrench, and an MCP tool (`mcp__server__name`) to
27
+ * the puzzle piece the MCP screens use, because "which server is this from" is
28
+ * the useful thing to see at a glance.
29
+ */
30
+ export function toolIcon(toolName: string): LucideIcon {
31
+ switch (toolName) {
32
+ case 'Bash':
33
+ case 'BashOutput':
34
+ case 'KillShell':
35
+ return Terminal
36
+ case 'Read':
37
+ return FileText
38
+ case 'Write':
39
+ return SquarePen
40
+ case 'Edit':
41
+ case 'MultiEdit':
42
+ case 'NotebookEdit':
43
+ return PencilLine
44
+ case 'Glob':
45
+ return FolderSearch
46
+ case 'Grep':
47
+ return Search
48
+ case 'WebFetch':
49
+ return ArrowDownCircle
50
+ case 'WebSearch':
51
+ return Globe
52
+ case 'Task':
53
+ case 'Agent':
54
+ return UsersRound
55
+ case 'TodoWrite':
56
+ return CheckSquare
57
+ case 'Skill':
58
+ return Sparkles
59
+ case 'AskUserQuestion':
60
+ return MessageCircleQuestion
61
+ // The codex engine's own tool names (see its runner's item mapping).
62
+ case 'CodexCommand':
63
+ return Terminal
64
+ case 'CodexFileChange':
65
+ return FileDiff
66
+ case 'CodexWebSearch':
67
+ return Globe
68
+ case 'CodexImageGeneration':
69
+ case 'CodexImageView':
70
+ return Image
71
+ default:
72
+ return toolName.startsWith('mcp__') ? Puzzle : Wrench
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Does this tool *change* the workspace?
78
+ *
79
+ * Worth its own colour in a transcript: skimming a run, "what did it edit" is a
80
+ * different question from "what did it look at", and a write is the one you
81
+ * might need to undo. Names from both first-party engines; an MCP tool is
82
+ * unknowable from its name, so it reads as neutral rather than guessed.
83
+ */
84
+ export function isMutatingTool(toolName: string): boolean {
85
+ switch (toolName) {
86
+ case 'Write':
87
+ case 'Edit':
88
+ case 'MultiEdit':
89
+ case 'NotebookEdit':
90
+ case 'Update':
91
+ case 'CodexFileChange':
92
+ return true
93
+ default:
94
+ return false
95
+ }
96
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The workspace layer — the VS Code-shaped layout around `SessionPanel`, and
3
+ * the Monaco editor at the middle of it.
4
+ *
5
+ * This is a **separate entry point** (`@workerdeck/ui/workspace`) for one
6
+ * reason: Monaco. It is only reachable from here, so an embedder who imports
7
+ * the root entry never pulls it into their module graph.
8
+ *
9
+ * That matters more than tree-shaking alone would suggest. Rollup does drop
10
+ * `CodeEditor` from a `SessionPanel`-only bundle — but Vite resolves Monaco's
11
+ * `new Worker(new URL(…, import.meta.url))` calls during *transform*, before
12
+ * tree-shaking runs, and emits ~9MB of language-service workers as assets that
13
+ * are never retracted. Keeping the import unreachable from the root entry is
14
+ * what actually prevents that; `sideEffects: false` does not.
15
+ *
16
+ * Consequently `monaco-editor` is an **optional peer dependency**: importing
17
+ * this entry means installing it, and importing only the root entry means not
18
+ * having to. See the README for the Vite configuration Monaco needs.
19
+ */
20
+
21
+ export {
22
+ SessionWorkspace,
23
+ type SessionWorkspaceProps,
24
+ } from './components/agent/SessionWorkspace.tsx'
25
+ export { FileTree, type FileTreeProps } from './components/agent/FileTree.tsx'
26
+ export { EditorTabs, type EditorTabsProps } from './components/agent/EditorTabs.tsx'
27
+ export { FileViewer, type FileViewerProps } from './components/agent/FileViewer.tsx'
28
+ export { CodeEditor, type CodeEditorProps } from './components/agent/CodeEditor.tsx'