@workerdeck/ui 2.9.0 → 2.10.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/index.d.mts +52 -6
- package/build/index.mjs +23 -9
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +20 -0
- package/build/{SessionPanel-BClL7zUS.mjs → use-path-links-CvcxdUXL.mjs} +437 -135
- package/build/use-path-links-CvcxdUXL.mjs.map +1 -0
- package/build/workspace.mjs +3 -83
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/Composer.tsx +7 -4
- package/src/components/agent/PromptTokenText.tsx +11 -3
- package/src/components/agent/SessionInfoDialog.tsx +1 -1
- package/src/components/agent/SessionItem.tsx +9 -5
- package/src/components/agent/SessionList.tsx +1 -1
- package/src/components/agent/SessionPanel.tsx +4 -1
- package/src/components/agent/StatusBar.tsx +1 -1
- package/src/components/agent/Transcript.tsx +83 -79
- package/src/components/agent/TranscriptItemView.tsx +1 -1
- package/src/components/agent/UsageDialog.tsx +87 -6
- package/src/components/agent/composer-commands.ts +14 -2
- package/src/components/agent/use-path-links.ts +5 -19
- package/src/components/prompt-area/clipboard-helpers.ts +13 -4
- package/src/components/prompt-area/dom-helpers.ts +10 -1
- package/src/components/prompt-area/index.ts +10 -1
- package/src/components/prompt-area/prompt-area-engine.ts +23 -14
- package/src/components/prompt-area/types.ts +20 -0
- package/src/components/prompt-area/use-prompt-area-keydown.ts +2 -1
- package/src/components/prompt-area/use-prompt-area.ts +8 -2
- package/src/components/terminal/StatusLine.tsx +3 -2
- package/src/components/terminal/items.tsx +3 -2
- package/src/index.ts +3 -1
- package/src/lib/path-match.ts +51 -0
- package/src/lib/session-cost.ts +65 -0
- package/build/SessionPanel-BClL7zUS.mjs.map +0 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
revertChipAtIndex,
|
|
11
11
|
replaceTextRange,
|
|
12
12
|
toggleMarkdownWrap,
|
|
13
|
+
chipPlainText,
|
|
13
14
|
} from './prompt-area-engine.ts'
|
|
14
15
|
import {
|
|
15
16
|
getListContext,
|
|
@@ -138,7 +139,7 @@ export function usePromptAreaKeydown({
|
|
|
138
139
|
if (s.type === 'text') {
|
|
139
140
|
targetOffset += s.text.length
|
|
140
141
|
} else {
|
|
141
|
-
targetOffset += s.
|
|
142
|
+
targetOffset += chipPlainText(s).length
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
targetOffset += result.revertedText.length
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
resolveChip,
|
|
13
13
|
resolveText,
|
|
14
14
|
truncateSegmentsToLength,
|
|
15
|
+
chipPlainText,
|
|
15
16
|
} from './prompt-area-engine.ts'
|
|
16
17
|
import {
|
|
17
18
|
autoFormatListPrefix,
|
|
@@ -223,6 +224,9 @@ export function usePromptArea({
|
|
|
223
224
|
chip.dataset.chipTrigger = seg.trigger
|
|
224
225
|
chip.dataset.chipValue = seg.value
|
|
225
226
|
chip.dataset.chipDisplay = seg.displayText
|
|
227
|
+
if (seg.sigil !== undefined) {
|
|
228
|
+
chip.dataset.chipSigil = seg.sigil
|
|
229
|
+
}
|
|
226
230
|
if (seg.data !== undefined) {
|
|
227
231
|
const json = safeJsonStringify(seg.data)
|
|
228
232
|
if (json) {
|
|
@@ -236,7 +240,7 @@ export function usePromptArea({
|
|
|
236
240
|
const chipStyle = triggerConfig?.chipStyle ?? 'pill'
|
|
237
241
|
chip.dataset.chipStyle = chipStyle
|
|
238
242
|
chip.className = cn('prompt-area-chip', chipStyle === 'inline' && 'prompt-area-chip--inline', triggerConfig?.chipClassName)
|
|
239
|
-
chip.textContent =
|
|
243
|
+
chip.textContent = chipPlainText(seg)
|
|
240
244
|
chip.setAttribute('role', 'button')
|
|
241
245
|
chip.setAttribute('tabindex', '-1')
|
|
242
246
|
editor.appendChild(chip)
|
|
@@ -598,10 +602,12 @@ export function usePromptArea({
|
|
|
598
602
|
}
|
|
599
603
|
|
|
600
604
|
const displayText = activeTrigger.config.onSelect?.(suggestion) ?? suggestion.label
|
|
605
|
+
const options = activeTrigger.config.chipOptions?.(suggestion)
|
|
601
606
|
|
|
602
607
|
const chipData = {
|
|
603
608
|
value: suggestion.value,
|
|
604
609
|
displayText: displayText || suggestion.label,
|
|
610
|
+
...(options?.sigil !== undefined ? { sigil: options.sigil } : {}),
|
|
605
611
|
data: suggestion.data,
|
|
606
612
|
}
|
|
607
613
|
|
|
@@ -616,7 +622,7 @@ export function usePromptArea({
|
|
|
616
622
|
}, 0)
|
|
617
623
|
return
|
|
618
624
|
}
|
|
619
|
-
const result = resolveChip(segments, activeTrigger, chipData)
|
|
625
|
+
const result = resolveChip(segments, activeTrigger, chipData, options?.trailingText)
|
|
620
626
|
|
|
621
627
|
onChange(result.segments)
|
|
622
628
|
renderSegmentsToDOM(result.segments)
|
|
@@ -75,10 +75,11 @@ export function TerminalStatusLine({
|
|
|
75
75
|
)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
const costUsd = state.costUsd ?? (state.totalCostUsd || undefined)
|
|
79
|
+
if (costUsd !== undefined && costUsd > 0) {
|
|
79
80
|
parts.push(
|
|
80
81
|
<Ink key="cost" tone="faint">
|
|
81
|
-
{formatCost(
|
|
82
|
+
{formatCost(costUsd)}
|
|
82
83
|
</Ink>,
|
|
83
84
|
)
|
|
84
85
|
}
|
|
@@ -3,6 +3,7 @@ import type { TranscriptItem } from '@workerdeck/react'
|
|
|
3
3
|
import { compactionText, formatBytes, formatCost, formatDuration, toolInputPreview } from '../../lib/format.ts'
|
|
4
4
|
import { isMutatingTool } from '../../lib/tool-icon.ts'
|
|
5
5
|
import { usePulse } from '../agent/pulse.tsx'
|
|
6
|
+
import { PromptTokenText } from '../agent/PromptTokenText.tsx'
|
|
6
7
|
import { BookmarkAction, CopyAction, WithActions } from './affordances.tsx'
|
|
7
8
|
import { TerminalDiff } from './diff.tsx'
|
|
8
9
|
import { TerminalMarkdown } from './markdown.tsx'
|
|
@@ -46,7 +47,7 @@ export function UserRow({ item }: { item: Extract<TranscriptItem, { kind: 'user'
|
|
|
46
47
|
{item.text
|
|
47
48
|
? item.text.split('\n').map((line, index) => (
|
|
48
49
|
<Row key={index} glyph={index === 0 ? PROMPT_GLYPH : undefined} glyphTone="dim" tone="fg">
|
|
49
|
-
{line
|
|
50
|
+
{line ? <PromptTokenText text={line} /> : ' '}
|
|
50
51
|
</Row>
|
|
51
52
|
))
|
|
52
53
|
: null}
|
|
@@ -270,7 +271,7 @@ export function TurnResultRow({ item }: { item: Extract<TranscriptItem, { kind:
|
|
|
270
271
|
return (
|
|
271
272
|
<div>
|
|
272
273
|
<Row tone={item.isError ? 'red' : 'faint'}>
|
|
273
|
-
{item.isError ? item.subtype : 'done'} · {formatDuration(item.durationMs)} · {formatCost(item.totalCostUsd)}
|
|
274
|
+
{item.isError ? item.subtype : 'done'} · {formatDuration(item.durationMs)} · {formatCost(item.costUsd ?? item.totalCostUsd)}
|
|
274
275
|
</Row>
|
|
275
276
|
{item.errors?.map((message, index) => (
|
|
276
277
|
<Row key={index} tone="red">
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,8 @@ export { Band, Blank, Ink, Row, type RowProps, type Tone } from './components/te
|
|
|
63
63
|
export { CopyAction, WithActions, useAffordances, type TerminalAffordances } from './components/terminal/affordances.tsx'
|
|
64
64
|
export { FileLinkProvider, useFileLinks, type FileLinkHandle, type FileLinkOpener } from './components/terminal/file-link.tsx'
|
|
65
65
|
export { parseFileLink, type FileLink } from './lib/file-link.ts'
|
|
66
|
+
export { matchPath, type PathHit, type PathMatch } from './lib/path-match.ts'
|
|
67
|
+
export { usePathLinks, resolveAgainstCwd } from './components/agent/use-path-links.ts'
|
|
66
68
|
export { Conversation, ConversationContent, ConversationScrollButton, type ConversationProps } from './components/agent/Conversation.tsx'
|
|
67
69
|
export { Message, MessageContent, type MessageProps } from './components/agent/Message.tsx'
|
|
68
70
|
export {
|
|
@@ -119,7 +121,7 @@ export { SkillsDialog, type SkillsDialogProps } from './components/agent/SkillsD
|
|
|
119
121
|
export { HostFilesDialog, type HostFilesDialogProps } from './components/agent/HostFilesDialog.tsx'
|
|
120
122
|
export { SessionList, SessionListItem, type SessionListItemProps, type SessionListProps } from './components/agent/SessionList.tsx'
|
|
121
123
|
export { SessionBrowser, rowShapeClass, type SessionBrowserProps } from './components/agent/SessionBrowser.tsx'
|
|
122
|
-
export { SessionItem, type SessionItemProps } from './components/agent/SessionItem.tsx'
|
|
124
|
+
export { SessionItem, type SelectModifiers, type SessionItemProps } from './components/agent/SessionItem.tsx'
|
|
123
125
|
export { SessionStatusIcon } from './components/agent/SessionStatusIcon.tsx'
|
|
124
126
|
export { EngineIcon, engineMark, vendorMarkClass, vendorTextClass } from './components/agent/EngineIcon.tsx'
|
|
125
127
|
export { type Step, StepRow, StepToggle, runningSteps, sessionSteps } from './components/agent/SessionSteps.tsx'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// One path segment: no whitespace, no separator, none of the characters that bracket a path in
|
|
2
|
+
// prose. `:` is out because it introduces `:line`.
|
|
3
|
+
const SEG = '[^\\s:\'"`()\\[\\]{}<>|]+'
|
|
4
|
+
|
|
5
|
+
// The first segment of a relative path additionally cannot start with `@`: in an `@file` mention
|
|
6
|
+
// (`@_docs/BACKLOG.md`) the `@` is the mention's sigil, not part of the name.
|
|
7
|
+
const HEAD = '[^\\s:\'"`()\\[\\]{}<>|@,]+'
|
|
8
|
+
|
|
9
|
+
// The match must start at a token boundary, never consumed into the path: unanchored,
|
|
10
|
+
// `@_docs/BACKLOG.md` matched the suffix `/BACKLOG.md` and opened at the filesystem root.
|
|
11
|
+
const PATH_PATTERN = new RegExp(`(?:^|[\\s'"\`([{<@,])((?:\\.\\.?)?(?:/${SEG})+|${HEAD}(?:/${SEG})+)(?::(\\d+))?`)
|
|
12
|
+
|
|
13
|
+
const FILE_PATTERN = /^([\w.@-]+\.[A-Za-z0-9]{1,10})(?::(\d+))?$/
|
|
14
|
+
|
|
15
|
+
const TRAILING = /[.,;:!?)\]}'"`]+$/
|
|
16
|
+
|
|
17
|
+
const FILENAME = /\.[A-Za-z0-9]{1,10}$/
|
|
18
|
+
|
|
19
|
+
export type PathHit = { path: string; line?: number }
|
|
20
|
+
|
|
21
|
+
export type PathMatch = PathHit & {
|
|
22
|
+
// How much of the inspected text the path accounted for, the "mostly a path" test the hover makes.
|
|
23
|
+
length: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function matchPath(text: string | null | undefined, inCode = false): PathMatch | undefined {
|
|
27
|
+
const source = text ?? ''
|
|
28
|
+
const match = PATH_PATTERN.exec(source)
|
|
29
|
+
if (!match) {
|
|
30
|
+
return inCode ? matchFilename(source) : undefined
|
|
31
|
+
}
|
|
32
|
+
const path = match[1]!.replace(TRAILING, '')
|
|
33
|
+
if (!path.includes('/') || path.endsWith('/')) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
const rooted = path.startsWith('/') || path.startsWith('./') || path.startsWith('../')
|
|
37
|
+
if (!rooted && !FILENAME.test(path)) {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
const line = match[2] ? Number(match[2]) : undefined
|
|
41
|
+
return { path, line, length: path.length + (match[2] ? match[2].length + 1 : 0) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function matchFilename(text: string): PathMatch | undefined {
|
|
45
|
+
const hit = FILE_PATTERN.exec(text.trim())
|
|
46
|
+
if (!hit) {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
const line = hit[2] ? Number(hit[2]) : undefined
|
|
50
|
+
return { path: hit[1]!, line, length: hit[0].length }
|
|
51
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ByModel,
|
|
3
|
+
type CostBreakdown,
|
|
4
|
+
type TokenUsage,
|
|
5
|
+
UNPRICED_WARN_SHARE,
|
|
6
|
+
canonicalModel,
|
|
7
|
+
costOf,
|
|
8
|
+
costOfByModel,
|
|
9
|
+
rateFor,
|
|
10
|
+
totalTokens,
|
|
11
|
+
} from '@workerdeck/protocol'
|
|
12
|
+
|
|
13
|
+
export type SessionCostRow = {
|
|
14
|
+
model: string
|
|
15
|
+
usage: TokenUsage
|
|
16
|
+
tokens: number
|
|
17
|
+
costUsd: number | undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SessionCost = {
|
|
21
|
+
displayUsd: number | undefined
|
|
22
|
+
computedUsd: number | undefined
|
|
23
|
+
reportedUsd: number | undefined
|
|
24
|
+
breakdown: CostBreakdown
|
|
25
|
+
rows: SessionCostRow[]
|
|
26
|
+
unpricedShare: number
|
|
27
|
+
showUnpricedWarning: boolean
|
|
28
|
+
// Both figures price the same tokens, one by the engine and one by us, so a gap is the thing worth reading.
|
|
29
|
+
gapUsd: number | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SessionCostInput = {
|
|
33
|
+
costUsd?: number
|
|
34
|
+
totalCostUsd?: number
|
|
35
|
+
usageByModel?: ByModel
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const GAP_SHARE = 0.02
|
|
39
|
+
|
|
40
|
+
export function sessionCost(input: SessionCostInput): SessionCost {
|
|
41
|
+
const byModel = input.usageByModel ?? {}
|
|
42
|
+
const breakdown = costOfByModel(byModel)
|
|
43
|
+
const computedUsd = input.costUsd ?? (breakdown.unpriced || Object.keys(byModel).length === 0 ? undefined : breakdown.total)
|
|
44
|
+
const reportedUsd = input.totalCostUsd
|
|
45
|
+
const rows = Object.entries(byModel)
|
|
46
|
+
.map(([model, usage]) => ({
|
|
47
|
+
model: canonicalModel(model),
|
|
48
|
+
usage,
|
|
49
|
+
tokens: totalTokens(usage),
|
|
50
|
+
costUsd: rateFor(model) ? costOf(usage, model).total : undefined,
|
|
51
|
+
}))
|
|
52
|
+
.sort((a, b) => b.tokens - a.tokens)
|
|
53
|
+
const gap = computedUsd !== undefined && reportedUsd !== undefined ? reportedUsd - computedUsd : undefined
|
|
54
|
+
const worthShowing = gap !== undefined && Math.abs(gap) > Math.max(0.01, GAP_SHARE * Math.max(computedUsd ?? 0, reportedUsd ?? 0))
|
|
55
|
+
return {
|
|
56
|
+
displayUsd: computedUsd ?? reportedUsd,
|
|
57
|
+
computedUsd,
|
|
58
|
+
reportedUsd,
|
|
59
|
+
breakdown,
|
|
60
|
+
rows,
|
|
61
|
+
unpricedShare: breakdown.unpricedShare,
|
|
62
|
+
showUnpricedWarning: breakdown.unpricedShare > UNPRICED_WARN_SHARE,
|
|
63
|
+
gapUsd: worthShowing ? gap : undefined,
|
|
64
|
+
}
|
|
65
|
+
}
|