@workerdeck/ui 2.8.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/{SessionPanel-_lQLhWOw.d.mts → SessionPanel-BVbau2eR.d.mts} +68 -4
- package/build/index.d.mts +62 -9
- package/build/index.mjs +24 -10
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +44 -0
- package/build/{SessionPanel-Dg4xf-ql.mjs → use-path-links-CvcxdUXL.mjs} +1080 -497
- package/build/use-path-links-CvcxdUXL.mjs.map +1 -0
- package/build/workspace.d.mts +3 -2
- package/build/workspace.mjs +5 -86
- package/build/workspace.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/agent/Composer.tsx +88 -83
- package/src/components/agent/PromptTokenText.tsx +11 -3
- package/src/components/agent/SessionInfoDialog.tsx +1 -1
- package/src/components/agent/SessionItem.tsx +12 -6
- package/src/components/agent/SessionList.tsx +1 -1
- package/src/components/agent/SessionPanel.tsx +308 -225
- package/src/components/agent/SessionWorkspace.tsx +3 -0
- 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/UsageMeters.tsx +17 -4
- package/src/components/agent/composer-commands.ts +154 -0
- package/src/components/agent/use-path-links.ts +7 -26
- 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/file-link.tsx +20 -0
- package/src/components/terminal/items.tsx +3 -2
- package/src/components/terminal/markdown.tsx +31 -5
- package/src/index.ts +14 -2
- package/src/lib/file-link.ts +70 -0
- package/src/lib/path-match.ts +51 -0
- package/src/lib/session-cost.ts +65 -0
- package/src/styles/terminal.css +15 -0
- package/build/SessionPanel-Dg4xf-ql.mjs.map +0 -1
|
@@ -11,13 +11,14 @@ import {
|
|
|
11
11
|
} from 'react'
|
|
12
12
|
import type { SkillInfo, SlashCommandInfo } from '@workerdeck/protocol'
|
|
13
13
|
import type { StagedAttachment, UseAttachmentsResult } from '@workerdeck/react'
|
|
14
|
-
import { ArrowUp, FileText, Paperclip, RotateCw, Sparkles, Square, TriangleAlert, X } from 'lucide-react'
|
|
14
|
+
import { ArrowUp, FileText, Paperclip, RotateCw, SlidersHorizontal, Sparkles, Square, TriangleAlert, X } from 'lucide-react'
|
|
15
15
|
import { Button } from '../ui/Button.tsx'
|
|
16
16
|
import { Spinner } from '../ui/Spinner.tsx'
|
|
17
17
|
import { PromptArea } from '../prompt-area/prompt-area.tsx'
|
|
18
18
|
import { usePromptAreaState } from '../prompt-area/use-prompt-area-state.ts'
|
|
19
19
|
import { plainTextToSegments } from '../prompt-area/prompt-area-engine.ts'
|
|
20
20
|
import { commandTrigger, launchTrigger, mentionTrigger } from '../prompt-area/trigger-presets.ts'
|
|
21
|
+
import { mergeComposerRows, rankComposerRows, skillTrailingText, type ClientCommand } from './composer-commands.ts'
|
|
21
22
|
import { useTranscriptVariant } from './transcript-variant.tsx'
|
|
22
23
|
import type { TerminalAffordances } from '../terminal/affordances.tsx'
|
|
23
24
|
import { PROMPT_GLYPH } from '../terminal/items.tsx'
|
|
@@ -44,6 +45,9 @@ export interface ComposerProps {
|
|
|
44
45
|
placeholder?: string
|
|
45
46
|
commands?: SlashCommandInfo[]
|
|
46
47
|
skills?: SkillInfo[]
|
|
48
|
+
// Session controls the host offers as `/` rows. Merged into the same ranked list as the engine's own
|
|
49
|
+
// commands, and suppressed name-for-name by an engine command so the real one always wins.
|
|
50
|
+
clientCommands?: ClientCommand[]
|
|
47
51
|
onSearchFiles?: (query: string, options: { signal: AbortSignal }) => Promise<ComposerFileMatch[]>
|
|
48
52
|
// Shell mode: `!` as the first character turns the composer into a host shell prompt. Omit to leave the
|
|
49
53
|
// mode off entirely - the gateway only offers it to an operator on a session whose engine reaches a host cwd.
|
|
@@ -61,24 +65,6 @@ export interface ComposerProps {
|
|
|
61
65
|
ref?: Ref<ComposerHandle>
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
function cleanName(name: string) {
|
|
65
|
-
return name.replace(/\s*\(MCP\)$/i, '')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function skillPrompt(skill: SkillInfo): string {
|
|
69
|
-
const base = skill.defaultPrompt?.trim() || `$${skill.name}`
|
|
70
|
-
return /\s$/.test(base) ? base : base + ' '
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function matchScore(query: string, haystacks: string[]): number {
|
|
74
|
-
const needle = query.toLowerCase()
|
|
75
|
-
const lowered = haystacks.map((s) => s.toLowerCase())
|
|
76
|
-
if (lowered.some((h) => h.startsWith(needle))) {
|
|
77
|
-
return 2
|
|
78
|
-
}
|
|
79
|
-
return lowered.some((h) => h.includes(needle)) ? 1 : 0
|
|
80
|
-
}
|
|
81
|
-
|
|
82
68
|
export function Composer({
|
|
83
69
|
onSend,
|
|
84
70
|
onInterrupt,
|
|
@@ -87,6 +73,7 @@ export function Composer({
|
|
|
87
73
|
placeholder = 'Message the agent…',
|
|
88
74
|
commands,
|
|
89
75
|
skills,
|
|
76
|
+
clientCommands,
|
|
90
77
|
onSearchFiles,
|
|
91
78
|
onShellCommand,
|
|
92
79
|
attachments,
|
|
@@ -109,6 +96,7 @@ export function Composer({
|
|
|
109
96
|
const fileInput = useRef<HTMLInputElement>(null)
|
|
110
97
|
const [dragging, setDragging] = useState(false)
|
|
111
98
|
const [shellMode, setShellMode] = useState(false)
|
|
99
|
+
const [helpOpen, setHelpOpen] = useState(false)
|
|
112
100
|
|
|
113
101
|
useImperativeHandle(
|
|
114
102
|
ref,
|
|
@@ -125,76 +113,46 @@ export function Composer({
|
|
|
125
113
|
|
|
126
114
|
const triggers = useMemo(() => {
|
|
127
115
|
const configured = []
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (seen.has(name)) {
|
|
134
|
-
return []
|
|
135
|
-
}
|
|
136
|
-
seen.add(name)
|
|
137
|
-
return [{ ...c, name }]
|
|
138
|
-
})
|
|
116
|
+
const rows = mergeComposerRows({ commands, clientCommands, skills })
|
|
117
|
+
if (rows.length > 0) {
|
|
118
|
+
// Keyed by kind as well as name: an engine command and a skill may share one, and a chip's `data`
|
|
119
|
+
// is serialized into the DOM, so the row itself never travels there.
|
|
120
|
+
const byTag = new Map(rows.map((row) => [`${row.kind}:${row.name}`, row]))
|
|
139
121
|
configured.push(
|
|
140
122
|
commandTrigger({
|
|
141
|
-
onSearch: (query: string): TriggerSuggestion[] =>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
123
|
+
onSearch: (query: string): TriggerSuggestion[] =>
|
|
124
|
+
rankComposerRows(query, rows).map((row) =>
|
|
125
|
+
row.kind === 'skill'
|
|
126
|
+
? {
|
|
127
|
+
value: row.name,
|
|
128
|
+
label: row.label,
|
|
129
|
+
description: row.description ? `Skill · ${row.description}` : 'Skill · inserts a message you can edit',
|
|
130
|
+
icon: <Sparkles className="size-3.5 text-fg-3" />,
|
|
131
|
+
data: `${row.kind}:${row.name}`,
|
|
132
|
+
}
|
|
133
|
+
: {
|
|
134
|
+
value: row.name,
|
|
135
|
+
label: `/${row.name}${row.argumentHint ? ` ${row.argumentHint}` : ''}`,
|
|
136
|
+
description: row.description,
|
|
137
|
+
icon: row.kind === 'client' ? <SlidersHorizontal className="size-3.5 text-fg-3" /> : undefined,
|
|
138
|
+
data: `${row.kind}:${row.name}`,
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
insertAsText: (suggestion) => {
|
|
142
|
+
const row = byTag.get(String(suggestion.data))
|
|
143
|
+
return row?.kind === 'client' && row.command.requiresArgs ? `/${row.name} ` : undefined
|
|
144
|
+
},
|
|
145
|
+
// A skill row came off the `/` menu but is codex's `$name` token, so the chip serialises with
|
|
146
|
+
// that sigil and the default prompt's remainder lands after it as editable text.
|
|
147
|
+
chipOptions: (suggestion) => {
|
|
148
|
+
const row = byTag.get(String(suggestion.data))
|
|
149
|
+
return row?.kind === 'skill' ? { sigil: '$', trailingText: skillTrailingText(row.skill) } : undefined
|
|
159
150
|
},
|
|
160
151
|
onSelect: (suggestion) => suggestion.value,
|
|
161
152
|
chipClassName: 'font-mono',
|
|
162
153
|
}),
|
|
163
154
|
)
|
|
164
155
|
}
|
|
165
|
-
if (usableSkills.length > 0) {
|
|
166
|
-
configured.push(
|
|
167
|
-
commandTrigger({
|
|
168
|
-
char: '$',
|
|
169
|
-
accessibilityLabel: 'skill',
|
|
170
|
-
onSearch: (query: string): TriggerSuggestion[] => {
|
|
171
|
-
const scored: Array<{ score: number; suggestion: TriggerSuggestion }> = []
|
|
172
|
-
for (const skill of usableSkills) {
|
|
173
|
-
const score = matchScore(query, [skill.name, ...skill.name.split(/[-:_]/)])
|
|
174
|
-
if (score === 0) {
|
|
175
|
-
continue
|
|
176
|
-
}
|
|
177
|
-
const summary = skill.shortDescription ?? skill.description
|
|
178
|
-
scored.push({
|
|
179
|
-
score,
|
|
180
|
-
suggestion: {
|
|
181
|
-
value: skill.name,
|
|
182
|
-
label: skill.displayName ?? skill.name,
|
|
183
|
-
description: summary ? `Skill · ${summary}` : 'Skill · inserts a message you can edit',
|
|
184
|
-
icon: <Sparkles className="size-3.5 text-fg-3" />,
|
|
185
|
-
},
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
scored.sort((a, b) => b.score - a.score)
|
|
189
|
-
return scored.map(({ suggestion }) => suggestion)
|
|
190
|
-
},
|
|
191
|
-
insertAsText: (suggestion) => {
|
|
192
|
-
const skill = usableSkills.find((s) => s.name === suggestion.value)
|
|
193
|
-
return skill ? skillPrompt(skill) : `$${suggestion.value} `
|
|
194
|
-
},
|
|
195
|
-
}),
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
156
|
if (onSearchFiles) {
|
|
199
157
|
configured.push(
|
|
200
158
|
mentionTrigger({
|
|
@@ -224,7 +182,7 @@ export function Composer({
|
|
|
224
182
|
)
|
|
225
183
|
}
|
|
226
184
|
return configured.length > 0 ? configured : undefined
|
|
227
|
-
}, [commands, skills, onSearchFiles, onShellCommand])
|
|
185
|
+
}, [commands, skills, clientCommands, onSearchFiles, onShellCommand])
|
|
228
186
|
|
|
229
187
|
const saveDraft = draft?.save
|
|
230
188
|
useEffect(() => {
|
|
@@ -261,6 +219,13 @@ export function Composer({
|
|
|
261
219
|
setShellMode(false)
|
|
262
220
|
focus()
|
|
263
221
|
}
|
|
222
|
+
|
|
223
|
+
const hints = [
|
|
224
|
+
triggers?.some((t) => t.char === '/') ? { key: '/', what: 'commands and skills' } : undefined,
|
|
225
|
+
onSearchFiles ? { key: '@', what: 'mention a file' } : undefined,
|
|
226
|
+
onShellCommand ? { key: '!', what: 'run a shell command' } : undefined,
|
|
227
|
+
{ key: '?', what: 'this list, on an empty composer' },
|
|
228
|
+
].filter((h) => h !== undefined)
|
|
264
229
|
// Escape and backspace-on-empty both leave, because both are what a person reaches for when the
|
|
265
230
|
// pink frame was not what they meant. Backspace only when there is nothing left to delete.
|
|
266
231
|
const shellKeys = shellMode
|
|
@@ -273,7 +238,40 @@ export function Composer({
|
|
|
273
238
|
}
|
|
274
239
|
},
|
|
275
240
|
}
|
|
276
|
-
: {
|
|
241
|
+
: {
|
|
242
|
+
onEscape: helpOpen ? () => setHelpOpen(false) : undefined,
|
|
243
|
+
// Only on a genuinely empty composer, so a message that opens with a question mark still types.
|
|
244
|
+
onKeyDown: (e: KeyboardEvent<HTMLDivElement>) => {
|
|
245
|
+
if (e.key === '?' && isEmpty && !e.metaKey && !e.ctrlKey) {
|
|
246
|
+
e.preventDefault()
|
|
247
|
+
setHelpOpen(true)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
if (helpOpen) {
|
|
251
|
+
setHelpOpen(false)
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const helpHints = helpOpen ? (
|
|
257
|
+
<>
|
|
258
|
+
{hints.map((hint) => (
|
|
259
|
+
<span key={hint.key} className="flex items-center gap-1">
|
|
260
|
+
<kbd className="font-mono text-text">{hint.key}</kbd>
|
|
261
|
+
{hint.what}
|
|
262
|
+
</span>
|
|
263
|
+
))}
|
|
264
|
+
</>
|
|
265
|
+
) : null
|
|
266
|
+
const helpRow = helpOpen ? (
|
|
267
|
+
<div
|
|
268
|
+
role="note"
|
|
269
|
+
aria-label="Composer shortcuts"
|
|
270
|
+
className="mx-auto mb-1 flex w-full max-w-[var(--wd-transcript-max-width)] flex-wrap items-center gap-x-3 gap-y-1 text-label text-fg-3"
|
|
271
|
+
>
|
|
272
|
+
{helpHints}
|
|
273
|
+
</div>
|
|
274
|
+
) : null
|
|
277
275
|
|
|
278
276
|
const pick = (files: FileList | null) => {
|
|
279
277
|
if (files && files.length > 0) {
|
|
@@ -405,6 +403,12 @@ export function Composer({
|
|
|
405
403
|
{submitButton}
|
|
406
404
|
</div>
|
|
407
405
|
</div>
|
|
406
|
+
{helpOpen ? (
|
|
407
|
+
<div role="note" aria-label="Composer shortcuts" className="term-row">
|
|
408
|
+
<span aria-hidden className="term-gutter" />
|
|
409
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-[2ch] text-fg-3">{helpHints}</div>
|
|
410
|
+
</div>
|
|
411
|
+
) : null}
|
|
408
412
|
{shellMode ? (
|
|
409
413
|
<div className="term-row">
|
|
410
414
|
<span aria-hidden className="term-gutter" />
|
|
@@ -425,6 +429,7 @@ export function Composer({
|
|
|
425
429
|
|
|
426
430
|
return (
|
|
427
431
|
<div data-slot="composer" className={cn('px-[var(--wd-composer-padding)] pb-[var(--wd-composer-padding)]', className)}>
|
|
432
|
+
{helpRow}
|
|
428
433
|
<div
|
|
429
434
|
{...dropHandlers}
|
|
430
435
|
className={cn(
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import { Fragment } from 'react'
|
|
1
|
+
import { createContext, Fragment, useContext, type ReactNode } from 'react'
|
|
2
2
|
import { scanPromptTokens } from '@workerdeck/react'
|
|
3
3
|
import { cn } from '../../lib/utils.ts'
|
|
4
4
|
|
|
5
|
+
const SkillNamesContext = createContext<readonly string[] | undefined>(undefined)
|
|
6
|
+
|
|
7
|
+
// The skill names a `$name` in a sent message may resolve to; unset, nothing is a skill token.
|
|
8
|
+
export function SkillNamesProvider({ names, children }: { names: readonly string[] | undefined; children: ReactNode }) {
|
|
9
|
+
return <SkillNamesContext.Provider value={names}>{children}</SkillNamesContext.Provider>
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export function PromptTokenText({ text, className }: { text: string; className?: string }) {
|
|
6
|
-
const
|
|
13
|
+
const skills = useContext(SkillNamesContext)
|
|
14
|
+
const tokens = scanPromptTokens(text, { skills })
|
|
7
15
|
if (tokens.length === 0) {
|
|
8
16
|
return <span className={className}>{text}</span>
|
|
9
17
|
}
|
|
10
|
-
const parts:
|
|
18
|
+
const parts: ReactNode[] = []
|
|
11
19
|
let cursor = 0
|
|
12
20
|
for (const [index, token] of tokens.entries()) {
|
|
13
21
|
if (token.start > cursor) {
|
|
@@ -46,7 +46,7 @@ export function SessionInfoDialog({ state, client, sessionId, open, onOpenChange
|
|
|
46
46
|
{session?.createdAt ? <DialogRow label="Started">{formatRelativeTime(session.createdAt)}</DialogRow> : null}
|
|
47
47
|
{session?.numTurns !== undefined ? <DialogRow label="Turns">{session.numTurns}</DialogRow> : null}
|
|
48
48
|
<DialogRow label="Cost" mono>
|
|
49
|
-
{formatCost(state.totalCostUsd)}
|
|
49
|
+
{formatCost(state.costUsd ?? (state.totalCostUsd || undefined))}
|
|
50
50
|
</DialogRow>
|
|
51
51
|
</div>
|
|
52
52
|
{state.capabilities.vfs ? <SessionFiles client={client} sessionId={sessionId} open={open} /> : null}
|
|
@@ -11,6 +11,8 @@ import type { Step } from './SessionSteps.tsx'
|
|
|
11
11
|
import { cn } from '../../lib/utils.ts'
|
|
12
12
|
import { formatCost, formatRelativeTime, friendlyModel } from '../../lib/format.ts'
|
|
13
13
|
|
|
14
|
+
export type SelectModifiers = { meta: boolean; ctrl: boolean; alt: boolean }
|
|
15
|
+
|
|
14
16
|
export interface SessionItemProps {
|
|
15
17
|
row: SessionRow
|
|
16
18
|
active?: boolean
|
|
@@ -20,7 +22,7 @@ export interface SessionItemProps {
|
|
|
20
22
|
projectIcons?: Record<string, string>
|
|
21
23
|
expanded?: boolean
|
|
22
24
|
onExpandedChange?: (expanded: boolean) => void
|
|
23
|
-
onSelect?: () => void
|
|
25
|
+
onSelect?: (modifiers: SelectModifiers) => void
|
|
24
26
|
onSelectSubagent?: (toolUseId: string) => void
|
|
25
27
|
onRename?: (title: string) => void
|
|
26
28
|
renameOn?: 'doubleClick' | 'external'
|
|
@@ -30,6 +32,8 @@ export interface SessionItemProps {
|
|
|
30
32
|
className?: string
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
const NO_MODIFIERS: SelectModifiers = { meta: false, ctrl: false, alt: false }
|
|
36
|
+
|
|
33
37
|
export function SessionItem({
|
|
34
38
|
row,
|
|
35
39
|
active = false,
|
|
@@ -66,7 +70,7 @@ export function SessionItem({
|
|
|
66
70
|
const project = showProject ? projectLabel(row) : projectSubpath(row)
|
|
67
71
|
const projectIcon = showProject ? info.project?.icon : undefined
|
|
68
72
|
const iconSrc = projectIcon?.type === 'image' ? projectIcons?.[projectIcon.hash] : undefined
|
|
69
|
-
const cost = formatCost(info.totalCostUsd)
|
|
73
|
+
const cost = formatCost(info.costUsd ?? info.totalCostUsd)
|
|
70
74
|
const extras = [
|
|
71
75
|
showGateway ? row.hostName : undefined,
|
|
72
76
|
info.profile ? `@${info.profile}` : undefined,
|
|
@@ -93,7 +97,7 @@ export function SessionItem({
|
|
|
93
97
|
for (const extra of extras) {
|
|
94
98
|
parts.push(<span key={extra}>{extra}</span>)
|
|
95
99
|
}
|
|
96
|
-
const steps = sessionSteps(info, (toolUseId) => (onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.()))
|
|
100
|
+
const steps = sessionSteps(info, (toolUseId) => (onSelectSubagent ? onSelectSubagent(toolUseId) : onSelect?.(NO_MODIFIERS)))
|
|
97
101
|
const holdsOpenAgent = steps.some((s) => s.key === activeStepKey)
|
|
98
102
|
|
|
99
103
|
return (
|
|
@@ -106,7 +110,7 @@ export function SessionItem({
|
|
|
106
110
|
if (e.detail > 1 || isEditing) {
|
|
107
111
|
return
|
|
108
112
|
}
|
|
109
|
-
onSelect?.()
|
|
113
|
+
onSelect?.({ meta: e.metaKey, ctrl: e.ctrlKey, alt: e.altKey })
|
|
110
114
|
}}
|
|
111
115
|
onKeyDown={(e) => {
|
|
112
116
|
if (e.target !== e.currentTarget) {
|
|
@@ -114,7 +118,7 @@ export function SessionItem({
|
|
|
114
118
|
}
|
|
115
119
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
116
120
|
e.preventDefault()
|
|
117
|
-
onSelect?.()
|
|
121
|
+
onSelect?.({ meta: e.metaKey, ctrl: e.ctrlKey, alt: e.altKey })
|
|
118
122
|
}
|
|
119
123
|
}}
|
|
120
124
|
className={cn(
|
|
@@ -162,7 +166,9 @@ export function SessionItem({
|
|
|
162
166
|
className={cn(
|
|
163
167
|
'flex h-4 min-w-6 shrink-0 items-center justify-center rounded-full px-2',
|
|
164
168
|
'text-[0.75rem] leading-none tracking-[-0.005em] tabular-nums',
|
|
165
|
-
|
|
169
|
+
// Grey while the turn is still producing, accent once it has stopped: the badge's colour
|
|
170
|
+
// answers "is this waiting for me", and a working session is not yet.
|
|
171
|
+
row.state === 'working' ? 'bg-badge text-badge-fg' : 'bg-accent text-accent-fg',
|
|
166
172
|
)}
|
|
167
173
|
>
|
|
168
174
|
{row.unseen}
|
|
@@ -36,7 +36,7 @@ export function SessionListItem({ session, active, onSelect, onDelete, className
|
|
|
36
36
|
<div className="mt-0.5 flex items-center gap-2 font-mono text-label text-fg-4">
|
|
37
37
|
<span className="truncate">{session.cwd}</span>
|
|
38
38
|
{session.profile ? <span className="shrink-0">@{session.profile}</span> : null}
|
|
39
|
-
<span className="shrink-0">{formatCost(session.totalCostUsd)}</span>
|
|
39
|
+
<span className="shrink-0">{formatCost(session.costUsd ?? session.totalCostUsd)}</span>
|
|
40
40
|
<span className="shrink-0">{formatRelativeTime(session.lastActivityAt ?? session.createdAt)}</span>
|
|
41
41
|
</div>
|
|
42
42
|
</button>
|