@workerdeck/ui 3.0.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/{SessionPanel-C1bU4yTM.d.mts → SessionPanel-BNNc8grK.d.mts} +9 -7
- package/build/index.d.mts +153 -45
- package/build/index.mjs +501 -226
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +15 -6
- package/build/{use-path-links-BEWxt0ZM.mjs → use-path-links-C6Fh5zdx.mjs} +1926 -2086
- package/build/use-path-links-C6Fh5zdx.mjs.map +1 -0
- package/build/workspace.d.mts +1 -1
- package/build/workspace.mjs +1 -1
- package/package.json +4 -4
- package/src/components/agent/ApprovalPrompts.tsx +87 -0
- package/src/components/agent/Composer.tsx +25 -47
- package/src/components/agent/ContextDialog.tsx +75 -67
- package/src/components/agent/FrameStrip.tsx +74 -0
- package/src/components/agent/HostFilesDialog.tsx +3 -2
- package/src/components/agent/McpDialog.tsx +112 -78
- package/src/components/agent/PermissionPrompt.tsx +13 -22
- package/src/components/agent/QuestionPrompt.tsx +6 -41
- package/src/components/agent/Scrubber.tsx +6 -75
- package/src/components/agent/SessionBrowser.tsx +125 -214
- package/src/components/agent/SessionFilters.tsx +266 -0
- package/src/components/agent/SessionInfoDialog.tsx +47 -63
- package/src/components/agent/SessionItem.tsx +27 -7
- package/src/components/agent/SessionPanel.tsx +244 -488
- package/src/components/agent/SessionSteps.tsx +54 -11
- package/src/components/agent/ShellStrip.tsx +27 -43
- package/src/components/agent/SubagentStrip.tsx +13 -30
- package/src/components/agent/Transcript.tsx +13 -0
- package/src/components/agent/TranscriptItemView.tsx +6 -36
- package/src/components/agent/composer-commands.ts +80 -1
- package/src/components/agent/markdown-image.tsx +49 -1
- package/src/components/agent/scrubber-marks.ts +33 -11
- package/src/components/agent/scrubber-peek.tsx +122 -0
- package/src/components/agent/session-panel-providers.tsx +41 -0
- package/src/components/agent/shell-actions.tsx +36 -1
- package/src/components/agent/use-catch-up.ts +20 -0
- package/src/components/agent/use-frame.ts +88 -0
- package/src/components/agent/use-host-image.ts +81 -0
- package/src/components/agent/use-shell-frame.ts +12 -70
- package/src/components/agent/use-subagent-frame.ts +19 -75
- package/src/components/prompt-area/prompt-area-engine.ts +21 -20
- package/src/components/prompt-area/prompt-area-list-ops.ts +12 -15
- package/src/components/terminal/PermissionPrompt.tsx +19 -32
- package/src/components/terminal/QuestionPrompt.tsx +10 -46
- package/src/components/terminal/TerminalTranscript.tsx +3 -86
- package/src/components/terminal/items.tsx +5 -43
- package/src/components/terminal/scrubber.tsx +25 -272
- package/src/components/ui/Popover.tsx +29 -0
- package/src/components/ui/Select.tsx +25 -0
- package/src/index.ts +46 -8
- package/src/lib/file-link.ts +3 -16
- package/src/lib/permission-prompt.ts +57 -0
- package/src/lib/question-answers.ts +65 -0
- package/build/use-path-links-BEWxt0ZM.mjs.map +0 -1
- package/src/components/prompt-area/use-markdown-mode.ts +0 -103
package/build/workspace.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as SessionPanelProps } from "./SessionPanel-BNNc8grK.mjs";
|
|
2
2
|
import { CSSProperties } from "react";
|
|
3
3
|
import { ShellInfo } from "@workerdeck/protocol";
|
|
4
4
|
import { OpenFile, UseHostFileSearchResult, UseHostFileTreeResult } from "@workerdeck/react";
|
package/build/workspace.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $t as
|
|
1
|
+
import { $t as Spinner, En as Input, N as ShellTerminal, Zt as Splitter, i as SessionPanel, in as TooltipProvider, jn as cn, kn as Button, n as usePathLinks, nn as Tip, q as EntryIcon, t as resolveAgainstCwd } from "./use-path-links-C6Fh5zdx.mjs";
|
|
2
2
|
import { m as formatBytes } from "./status-kmsBhuFG.mjs";
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workerdeck/ui",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Styled agent-control component library for WorkerDeck: session panel, transcript with streaming, tool-call cards, permission prompts, session list, composer. Tailwind v4 + Base UI + cva. Pairs with the headless @workerdeck/react layer.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"streamdown": "^2.6.0",
|
|
40
40
|
"tailwind-merge": "^3.7.0",
|
|
41
41
|
"use-stick-to-bottom": "^1.1.6",
|
|
42
|
-
"@workerdeck/client": "3.0
|
|
43
|
-
"@workerdeck/
|
|
44
|
-
"@workerdeck/
|
|
42
|
+
"@workerdeck/client": "3.2.0",
|
|
43
|
+
"@workerdeck/protocol": "3.2.0",
|
|
44
|
+
"@workerdeck/react": "3.2.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"monaco-editor": "^0.56.0",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react'
|
|
2
|
+
import type { PermissionRequest } from '@workerdeck/protocol'
|
|
3
|
+
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
import { PermissionPrompt } from './PermissionPrompt.tsx'
|
|
5
|
+
import { QuestionPrompt, parseUserQuestions } from './QuestionPrompt.tsx'
|
|
6
|
+
import { TerminalPermissionPrompt } from '../terminal/PermissionPrompt.tsx'
|
|
7
|
+
import { TerminalQuestionPrompt } from '../terminal/QuestionPrompt.tsx'
|
|
8
|
+
import { TerminalSurface } from '../terminal/surface.tsx'
|
|
9
|
+
import type { TerminalAffordances } from '../terminal/affordances.tsx'
|
|
10
|
+
|
|
11
|
+
// Deliberately the built-in prompts' own callback shapes, so `PermissionPrompt` and
|
|
12
|
+
// `TerminalPermissionPrompt` stay drop-in fallbacks for an input a host declines to draw.
|
|
13
|
+
export interface ApprovalPromptProps {
|
|
14
|
+
request: PermissionRequest
|
|
15
|
+
onApprove: (requestId: string, updatedInput?: Record<string, unknown>) => void
|
|
16
|
+
onDeny: (requestId: string, message?: string, interrupt?: boolean) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ApprovalPromptsProps = {
|
|
20
|
+
requests: readonly PermissionRequest[]
|
|
21
|
+
terminal: boolean
|
|
22
|
+
fontSize?: number
|
|
23
|
+
lineHeight?: number
|
|
24
|
+
affordances?: TerminalAffordances | boolean
|
|
25
|
+
hostPrompts?: Record<string, ComponentType<ApprovalPromptProps>>
|
|
26
|
+
onApprove: ApprovalPromptProps['onApprove']
|
|
27
|
+
onDeny: ApprovalPromptProps['onDeny']
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ApprovalPrompts({
|
|
31
|
+
requests,
|
|
32
|
+
terminal,
|
|
33
|
+
fontSize,
|
|
34
|
+
lineHeight,
|
|
35
|
+
affordances,
|
|
36
|
+
hostPrompts,
|
|
37
|
+
onApprove,
|
|
38
|
+
onDeny,
|
|
39
|
+
}: ApprovalPromptsProps) {
|
|
40
|
+
const dismissQuestion = (id: string) => onDeny(id, 'Question dismissed by user')
|
|
41
|
+
return (
|
|
42
|
+
<div className={cn(terminal ? 'pb-2' : 'px-3 pb-2')}>
|
|
43
|
+
<PromptSurface terminal={terminal} fontSize={fontSize} lineHeight={lineHeight} affordances={affordances}>
|
|
44
|
+
{requests.map((request) => {
|
|
45
|
+
// Before the variant split: a host's entry is the renderer for its tool in
|
|
46
|
+
// both themes, and it overrides the built-in entries rather than racing them.
|
|
47
|
+
const HostPrompt = hostPrompts?.[request.toolName]
|
|
48
|
+
if (HostPrompt) {
|
|
49
|
+
return <HostPrompt key={request.id} request={request} onApprove={onApprove} onDeny={onDeny} />
|
|
50
|
+
}
|
|
51
|
+
const isQuestion = request.toolName === 'AskUserQuestion' && parseUserQuestions(request.input).length > 0
|
|
52
|
+
if (terminal) {
|
|
53
|
+
return isQuestion ? (
|
|
54
|
+
<TerminalQuestionPrompt key={request.id} request={request} onAnswer={onApprove} onDismiss={dismissQuestion} />
|
|
55
|
+
) : (
|
|
56
|
+
<TerminalPermissionPrompt key={request.id} request={request} onApprove={onApprove} onDeny={onDeny} />
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
return isQuestion ? (
|
|
60
|
+
<QuestionPrompt key={request.id} request={request} onAnswer={onApprove} onDismiss={dismissQuestion} />
|
|
61
|
+
) : (
|
|
62
|
+
<PermissionPrompt key={request.id} request={request} onApprove={onApprove} onDeny={onDeny} />
|
|
63
|
+
)
|
|
64
|
+
})}
|
|
65
|
+
</PromptSurface>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type PromptSurfaceProps = {
|
|
71
|
+
terminal: boolean
|
|
72
|
+
fontSize?: number
|
|
73
|
+
lineHeight?: number
|
|
74
|
+
affordances?: TerminalAffordances | boolean
|
|
75
|
+
children: ReactNode
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function PromptSurface({ terminal, fontSize, lineHeight, affordances, children }: PromptSurfaceProps) {
|
|
79
|
+
if (!terminal) {
|
|
80
|
+
return <div className="mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] flex-col gap-2">{children}</div>
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
<TerminalSurface fontSize={fontSize} lineHeight={lineHeight} affordances={affordances} bleed="1ch" className="term-transcript">
|
|
84
|
+
{children}
|
|
85
|
+
</TerminalSurface>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -394,6 +394,26 @@ export function Composer({
|
|
|
394
394
|
</div>
|
|
395
395
|
) : null
|
|
396
396
|
|
|
397
|
+
const stagedStrip = staged.length > 0 && attachments ? <AttachmentStrip attachments={attachments} /> : null
|
|
398
|
+
|
|
399
|
+
const field = (minHeight: number, maxHeight: number, fieldClass: string) => (
|
|
400
|
+
<PromptArea
|
|
401
|
+
{...bind}
|
|
402
|
+
triggers={triggers}
|
|
403
|
+
markdown={false}
|
|
404
|
+
normalizeBullets={false}
|
|
405
|
+
onSubmit={submit}
|
|
406
|
+
disabled={disabled}
|
|
407
|
+
placeholder={disabled ? 'Session ended' : shellMode ? SHELL_PLACEHOLDER : placeholder}
|
|
408
|
+
minHeight={minHeight}
|
|
409
|
+
maxHeight={maxHeight}
|
|
410
|
+
aria-label={shellMode ? 'Run a shell command' : 'Message the agent'}
|
|
411
|
+
className={fieldClass}
|
|
412
|
+
onImagePaste={(file) => attachments?.add([file])}
|
|
413
|
+
{...shellKeys}
|
|
414
|
+
/>
|
|
415
|
+
)
|
|
416
|
+
|
|
397
417
|
if (terminal) {
|
|
398
418
|
const line = lineHeight ?? 18
|
|
399
419
|
return (
|
|
@@ -408,26 +428,12 @@ export function Composer({
|
|
|
408
428
|
className={cn('term-composer', shellMode && 'term-composer-shell', disabled && 'opacity-60')}
|
|
409
429
|
>
|
|
410
430
|
<div className="term-composer-body">
|
|
411
|
-
{
|
|
431
|
+
{stagedStrip}
|
|
412
432
|
<div className="term-row">
|
|
413
433
|
{canAttach && !shellMode ? fileField : null}
|
|
414
434
|
{gutter}
|
|
415
435
|
<div className="flex min-w-0 items-start">
|
|
416
|
-
|
|
417
|
-
{...bind}
|
|
418
|
-
triggers={triggers}
|
|
419
|
-
markdown={false}
|
|
420
|
-
normalizeBullets={false}
|
|
421
|
-
onSubmit={submit}
|
|
422
|
-
disabled={disabled}
|
|
423
|
-
placeholder={disabled ? 'Session ended' : shellMode ? SHELL_PLACEHOLDER : placeholder}
|
|
424
|
-
minHeight={line}
|
|
425
|
-
maxHeight={line * 10}
|
|
426
|
-
aria-label={shellMode ? 'Run a shell command' : 'Message the agent'}
|
|
427
|
-
className="term-composer-field min-w-0 flex-1"
|
|
428
|
-
onImagePaste={(file) => attachments?.add([file])}
|
|
429
|
-
{...shellKeys}
|
|
430
|
-
/>
|
|
436
|
+
{field(line, line * 10, 'term-composer-field min-w-0 flex-1')}
|
|
431
437
|
{submitButton}
|
|
432
438
|
</div>
|
|
433
439
|
</div>
|
|
@@ -470,44 +476,16 @@ export function Composer({
|
|
|
470
476
|
disabled && 'opacity-60',
|
|
471
477
|
)}
|
|
472
478
|
>
|
|
473
|
-
{
|
|
479
|
+
{stagedStrip}
|
|
474
480
|
{inline ? (
|
|
475
481
|
<div className="flex items-end gap-1 p-1">
|
|
476
482
|
{shellMode ? <ShellBadge onLeave={() => leaveShellMode(false)} /> : attach}
|
|
477
|
-
|
|
478
|
-
{...bind}
|
|
479
|
-
triggers={triggers}
|
|
480
|
-
markdown={false}
|
|
481
|
-
normalizeBullets={false}
|
|
482
|
-
onSubmit={submit}
|
|
483
|
-
disabled={disabled}
|
|
484
|
-
placeholder={disabled ? 'Session ended' : shellMode ? SHELL_PLACEHOLDER : placeholder}
|
|
485
|
-
minHeight={20}
|
|
486
|
-
maxHeight={192}
|
|
487
|
-
aria-label={shellMode ? 'Run a shell command' : 'Message the agent'}
|
|
488
|
-
className="min-w-0 flex-1 py-1 text-body-sm text-text"
|
|
489
|
-
onImagePaste={(file) => attachments?.add([file])}
|
|
490
|
-
{...shellKeys}
|
|
491
|
-
/>
|
|
483
|
+
{field(20, 192, 'min-w-0 flex-1 py-1 text-body-sm text-text')}
|
|
492
484
|
{submitButton}
|
|
493
485
|
</div>
|
|
494
486
|
) : (
|
|
495
487
|
<>
|
|
496
|
-
|
|
497
|
-
{...bind}
|
|
498
|
-
triggers={triggers}
|
|
499
|
-
markdown={false}
|
|
500
|
-
normalizeBullets={false}
|
|
501
|
-
onSubmit={submit}
|
|
502
|
-
disabled={disabled}
|
|
503
|
-
placeholder={disabled ? 'Session ended' : shellMode ? SHELL_PLACEHOLDER : placeholder}
|
|
504
|
-
minHeight={28}
|
|
505
|
-
maxHeight={192}
|
|
506
|
-
aria-label={shellMode ? 'Run a shell command' : 'Message the agent'}
|
|
507
|
-
className="px-3 pt-2.5 pb-0 text-body-sm text-text"
|
|
508
|
-
onImagePaste={(file) => attachments?.add([file])}
|
|
509
|
-
{...shellKeys}
|
|
510
|
-
/>
|
|
488
|
+
{field(28, 192, 'px-3 pt-2.5 pb-0 text-body-sm text-text')}
|
|
511
489
|
<div className="flex items-center justify-between gap-2 px-2 pb-2">
|
|
512
490
|
<div className="flex min-w-0 items-center gap-1">
|
|
513
491
|
{shellMode ? <ShellBadge onLeave={() => leaveShellMode(false)} /> : attach}
|
|
@@ -14,81 +14,89 @@ export interface ContextDialogProps {
|
|
|
14
14
|
className?: string
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface ContextPanelProps {
|
|
18
|
+
usage?: ContextUsage
|
|
19
|
+
engine?: ProfileEngine
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export function ContextDialog({ usage, engine, open, onOpenChange, className }: ContextDialogProps) {
|
|
18
|
-
const note = contextNote(engine)
|
|
19
23
|
return (
|
|
20
24
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
21
25
|
<DialogContent className={className}>
|
|
22
26
|
<DialogHeader title="Context" description={usage?.model} />
|
|
23
27
|
<DialogBody>
|
|
24
|
-
{
|
|
25
|
-
<p className="py-6 text-center text-body-sm text-fg-4">
|
|
26
|
-
No reading yet - the context window is measured after a turn completes.
|
|
27
|
-
</p>
|
|
28
|
-
) : (
|
|
29
|
-
<>
|
|
30
|
-
<div className="flex items-baseline justify-between gap-4">
|
|
31
|
-
<span className="text-label text-fg-3">Used</span>
|
|
32
|
-
<span className="font-mono text-body-sm text-fg-1">
|
|
33
|
-
{formatTokens(usage.totalTokens)} / {formatTokens(usage.maxTokens)} · {usage.percentage.toFixed(0)}%
|
|
34
|
-
</span>
|
|
35
|
-
</div>
|
|
36
|
-
<div className="mt-2 h-2 overflow-hidden rounded-full bg-border">
|
|
37
|
-
<div
|
|
38
|
-
className={cn('h-full rounded-full', meterTintClass(usage.percentage))}
|
|
39
|
-
style={{ width: `${Math.min(100, Math.max(2, usage.percentage))}%` }}
|
|
40
|
-
/>
|
|
41
|
-
</div>
|
|
42
|
-
{usage.categories.length > 0 ? (
|
|
43
|
-
<div className="mt-5">
|
|
44
|
-
<h3 className="text-label font-medium text-fg-3">Breakdown</h3>
|
|
45
|
-
<div className="mt-2 flex flex-col gap-3">
|
|
46
|
-
{usage.categories.map((category) => {
|
|
47
|
-
const share = (category.tokens / Math.max(usage.maxTokens, 1)) * 100
|
|
48
|
-
const color = cssColor(category.color)
|
|
49
|
-
return (
|
|
50
|
-
<div key={category.name}>
|
|
51
|
-
<div className="flex items-baseline justify-between gap-3">
|
|
52
|
-
<span className="flex min-w-0 items-center gap-2">
|
|
53
|
-
<span
|
|
54
|
-
className="size-2 shrink-0 rounded-full bg-fg-4"
|
|
55
|
-
style={color ? { backgroundColor: color } : undefined}
|
|
56
|
-
/>
|
|
57
|
-
<span className="truncate text-body-sm text-fg-1">{category.name}</span>
|
|
58
|
-
</span>
|
|
59
|
-
<span className="shrink-0 font-mono text-label text-fg-3">{formatTokens(category.tokens)}</span>
|
|
60
|
-
</div>
|
|
61
|
-
<div className="mt-1 h-1 overflow-hidden rounded-full bg-border">
|
|
62
|
-
<div
|
|
63
|
-
className="h-full rounded-full bg-fg-4"
|
|
64
|
-
style={{
|
|
65
|
-
width: `${Math.min(100, share)}%`,
|
|
66
|
-
backgroundColor: color,
|
|
67
|
-
}}
|
|
68
|
-
/>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
)
|
|
72
|
-
})}
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
) : null}
|
|
76
|
-
</>
|
|
77
|
-
)}
|
|
78
|
-
{note ? (
|
|
79
|
-
<div className="mt-5 border-t border-border pt-4">
|
|
80
|
-
<h3 className="text-label font-medium text-fg-3">Why this window is 272K</h3>
|
|
81
|
-
<p className="mt-2 text-body-sm text-fg-2">{note.summary}</p>
|
|
82
|
-
{note.detail.map((line) => (
|
|
83
|
-
<p key={line} className="mt-2 text-body-sm text-fg-3">
|
|
84
|
-
{line}
|
|
85
|
-
</p>
|
|
86
|
-
))}
|
|
87
|
-
<p className="mt-2 text-body-sm text-warning">{note.caveat}</p>
|
|
88
|
-
</div>
|
|
89
|
-
) : null}
|
|
28
|
+
<ContextPanel usage={usage} engine={engine} />
|
|
90
29
|
</DialogBody>
|
|
91
30
|
</DialogContent>
|
|
92
31
|
</Dialog>
|
|
93
32
|
)
|
|
94
33
|
}
|
|
34
|
+
|
|
35
|
+
export function ContextPanel({ usage, engine }: ContextPanelProps) {
|
|
36
|
+
const note = contextNote(engine)
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
{!usage ? (
|
|
40
|
+
<p className="py-6 text-center text-body-sm text-fg-4">No reading yet - the context window is measured after a turn completes.</p>
|
|
41
|
+
) : (
|
|
42
|
+
<>
|
|
43
|
+
<div className="flex items-baseline justify-between gap-4">
|
|
44
|
+
<span className="text-label text-fg-3">Used</span>
|
|
45
|
+
<span className="font-mono text-body-sm text-fg-1">
|
|
46
|
+
{formatTokens(usage.totalTokens)} / {formatTokens(usage.maxTokens)} · {usage.percentage.toFixed(0)}%
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="mt-2 h-2 overflow-hidden rounded-full bg-border">
|
|
50
|
+
<div
|
|
51
|
+
className={cn('h-full rounded-full', meterTintClass(usage.percentage))}
|
|
52
|
+
style={{ width: `${Math.min(100, Math.max(2, usage.percentage))}%` }}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
{usage.categories.length > 0 ? (
|
|
56
|
+
<div className="mt-5">
|
|
57
|
+
<h3 className="text-label font-medium text-fg-3">Breakdown</h3>
|
|
58
|
+
<div className="mt-2 flex flex-col gap-3">
|
|
59
|
+
{usage.categories.map((category) => {
|
|
60
|
+
const share = (category.tokens / Math.max(usage.maxTokens, 1)) * 100
|
|
61
|
+
const color = cssColor(category.color)
|
|
62
|
+
return (
|
|
63
|
+
<div key={category.name}>
|
|
64
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
65
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
66
|
+
<span className="size-2 shrink-0 rounded-full bg-fg-4" style={color ? { backgroundColor: color } : undefined} />
|
|
67
|
+
<span className="truncate text-body-sm text-fg-1">{category.name}</span>
|
|
68
|
+
</span>
|
|
69
|
+
<span className="shrink-0 font-mono text-label text-fg-3">{formatTokens(category.tokens)}</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="mt-1 h-1 overflow-hidden rounded-full bg-border">
|
|
72
|
+
<div
|
|
73
|
+
className="h-full rounded-full bg-fg-4"
|
|
74
|
+
style={{
|
|
75
|
+
width: `${Math.min(100, share)}%`,
|
|
76
|
+
backgroundColor: color,
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
})}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
) : null}
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
{note ? (
|
|
89
|
+
<div className="mt-5 border-t border-border pt-4">
|
|
90
|
+
<h3 className="text-label font-medium text-fg-3">Why this window is 272K</h3>
|
|
91
|
+
<p className="mt-2 text-body-sm text-fg-2">{note.summary}</p>
|
|
92
|
+
{note.detail.map((line) => (
|
|
93
|
+
<p key={line} className="mt-2 text-body-sm text-fg-3">
|
|
94
|
+
{line}
|
|
95
|
+
</p>
|
|
96
|
+
))}
|
|
97
|
+
<p className="mt-2 text-body-sm text-warning">{note.caveat}</p>
|
|
98
|
+
</div>
|
|
99
|
+
) : null}
|
|
100
|
+
</>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { ArrowLeft } from 'lucide-react'
|
|
3
|
+
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
import { Button } from '../ui/Button.tsx'
|
|
5
|
+
import { Ink, Row } from '../terminal/row.tsx'
|
|
6
|
+
import { TerminalSurface } from '../terminal/surface.tsx'
|
|
7
|
+
import { WithActions } from '../terminal/affordances.tsx'
|
|
8
|
+
|
|
9
|
+
export type FrameKind = 'shell' | 'subagent'
|
|
10
|
+
|
|
11
|
+
export interface FrameStripProps {
|
|
12
|
+
kind: FrameKind
|
|
13
|
+
name: string
|
|
14
|
+
status?: string
|
|
15
|
+
detail?: string
|
|
16
|
+
failed: boolean
|
|
17
|
+
live: boolean
|
|
18
|
+
onBack: () => void
|
|
19
|
+
// Drawn at the strip's trailing edge; a terminal strip then hangs them inline, which is why only the name stays the Back target.
|
|
20
|
+
actions?: ReactNode
|
|
21
|
+
terminal: boolean
|
|
22
|
+
fontSize?: number
|
|
23
|
+
lineHeight?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TERMINAL_TONE = { shell: { name: 'magenta', live: 'magenta' }, subagent: { name: 'green', live: 'mark' } } as const
|
|
27
|
+
const CARDS_LIVE_CLASS: Record<FrameKind, string> = { shell: 'text-[var(--wd-shell-accent)]', subagent: 'text-accent' }
|
|
28
|
+
|
|
29
|
+
export function FrameStrip({ kind, name, status, detail, failed, live, onBack, actions, terminal, fontSize, lineHeight }: FrameStripProps) {
|
|
30
|
+
if (terminal) {
|
|
31
|
+
const tones = TERMINAL_TONE[kind]
|
|
32
|
+
const line = (label: ReactNode) => (
|
|
33
|
+
<Row glyph="←" glyphTone="dim" indent={1} tone={failed ? 'red' : tones.name}>
|
|
34
|
+
{label}
|
|
35
|
+
{status ? <Ink tone={failed ? 'red' : live ? tones.live : 'dim'}> · {status}</Ink> : null}
|
|
36
|
+
{detail ? <Ink tone="faint"> · {detail}</Ink> : null}
|
|
37
|
+
</Row>
|
|
38
|
+
)
|
|
39
|
+
return (
|
|
40
|
+
<TerminalSurface fontSize={fontSize} lineHeight={lineHeight} className="shrink-0">
|
|
41
|
+
{actions === undefined ? (
|
|
42
|
+
<button type="button" onClick={onBack} aria-label="Back to the session" className="block w-full cursor-pointer text-left">
|
|
43
|
+
{line(name)}
|
|
44
|
+
</button>
|
|
45
|
+
) : (
|
|
46
|
+
<WithActions placement="inline" actions={actions}>
|
|
47
|
+
{line(
|
|
48
|
+
<button type="button" onClick={onBack} aria-label="Back to the session" className="cursor-pointer text-left">
|
|
49
|
+
{name}
|
|
50
|
+
</button>,
|
|
51
|
+
)}
|
|
52
|
+
</WithActions>
|
|
53
|
+
)}
|
|
54
|
+
</TerminalSurface>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-1.5">
|
|
60
|
+
<Button variant="ghost" size="sm" onClick={onBack} className="h-6 gap-1 px-1.5">
|
|
61
|
+
<ArrowLeft className="size-3.5" />
|
|
62
|
+
Back
|
|
63
|
+
</Button>
|
|
64
|
+
<span className={cn('min-w-0 flex-1 truncate text-body-sm', kind === 'shell' && 'font-mono', failed ? 'text-danger' : 'text-fg-2')}>
|
|
65
|
+
{name}
|
|
66
|
+
</span>
|
|
67
|
+
{status ? (
|
|
68
|
+
<span className={cn('shrink-0 text-label', failed ? 'text-danger' : live ? CARDS_LIVE_CLASS[kind] : 'text-fg-3')}>{status}</span>
|
|
69
|
+
) : null}
|
|
70
|
+
{detail ? <span className="shrink-0 text-label text-fg-4">{detail}</span> : null}
|
|
71
|
+
{actions}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorMessage } from '@workerdeck/protocol'
|
|
1
2
|
import { useCallback, useEffect, useState } from 'react'
|
|
2
3
|
import type { WorkerDeckClient } from '@workerdeck/client'
|
|
3
4
|
import type { HostDirEntry, HostFileMatch } from '@workerdeck/protocol'
|
|
@@ -37,7 +38,7 @@ export function HostFilesDialog({ client, cwd, open, onOpenChange }: HostFilesDi
|
|
|
37
38
|
setEntries(response.entries)
|
|
38
39
|
setTruncated(response.truncated ?? false)
|
|
39
40
|
} catch (e) {
|
|
40
|
-
setError(e
|
|
41
|
+
setError(errorMessage(e, 'Could not read that directory'))
|
|
41
42
|
} finally {
|
|
42
43
|
setLoading(false)
|
|
43
44
|
}
|
|
@@ -87,7 +88,7 @@ export function HostFilesDialog({ client, cwd, open, onOpenChange }: HostFilesDi
|
|
|
87
88
|
content: response.encoding === 'utf8' ? response.content : '(binary file - not shown)',
|
|
88
89
|
})
|
|
89
90
|
} catch (e) {
|
|
90
|
-
setError(e
|
|
91
|
+
setError(errorMessage(e, 'Could not read that file'))
|
|
91
92
|
} finally {
|
|
92
93
|
setLoading(false)
|
|
93
94
|
}
|