@workerdeck/ui 2.11.0 → 2.12.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.
@@ -10,7 +10,7 @@ import { subagentItems, terminalBlocks, type ToolCallItem } from '../terminal/bl
10
10
  import { taskBrief, taskBusy } from '../terminal/tool-run.ts'
11
11
  import { type TranscriptRow } from './transcript-rows.ts'
12
12
  import { TranscriptRows } from './TranscriptRows.tsx'
13
- import { SkillNamesProvider } from './PromptTokenText.tsx'
13
+ import { SessionNamesProvider, SkillNamesProvider } from './PromptTokenText.tsx'
14
14
  import { TerminalSurface } from '../terminal/surface.tsx'
15
15
  import { ROW_GAP, TranscriptVariantProvider, type TranscriptDensity, type TranscriptVariant } from './transcript-variant.tsx'
16
16
 
@@ -79,6 +79,8 @@ export interface TranscriptProps {
79
79
  stickyPrompt?: boolean
80
80
  scrubber?: boolean
81
81
  bookmarks?: readonly string[]
82
+ // Folded peer slugs, so a `#Name` this client can name draws as a token.
83
+ sessionNames?: readonly string[]
82
84
  replaying?: boolean
83
85
  catchUp?: { from: number; since?: number }
84
86
  jumpToRecapRef?: RefObject<(() => void) | null>
@@ -104,6 +106,7 @@ export function Transcript({
104
106
  stickyPrompt = false,
105
107
  scrubber,
106
108
  bookmarks,
109
+ sessionNames,
107
110
  replaying = false,
108
111
  catchUp,
109
112
  jumpToRecapRef,
@@ -141,88 +144,96 @@ export function Transcript({
141
144
  return (
142
145
  <TranscriptVariantProvider value={variant}>
143
146
  <SkillNamesProvider names={skillNames}>
144
- <Conversation className={cn(replaying && 'invisible', className)}>
145
- <ConversationContent className={cn(terminal && 'gap-0 p-0')}>
146
- <TerminalShell active={terminal} fontSize={fontSize} lineHeight={lineHeight} affordances={affordances}>
147
- {frame ? (
148
- frameTask === undefined && !replaying ? (
149
- <div className={cn(terminal ? 'term-row text-fg-4' : 'p-4 text-body-sm text-fg-4')}>
150
- This sub-agent's work is not in this transcript.
151
- </div>
152
- ) : null
153
- ) : items.length === 0 && state.status !== 'starting' ? (
154
- emptyState !== undefined ? (
155
- emptyState
156
- ) : (
157
- <SessionEmptyState
158
- cwd={state.cwd}
159
- hasCommands={!!state.commands?.length}
160
- hasSkills={!!state.skills?.some((s) => s.enabled)}
161
- canBrowseFiles={canBrowseFiles}
162
- />
163
- )
164
- ) : null}
165
- {frame && frameTask === undefined && !replaying ? null : (
166
- <TranscriptRows
167
- rows={rows}
168
- boundary={boundary}
169
- since={catchUp?.since}
170
- terminal={terminal}
171
- replaying={replaying}
172
- stickyPrompt={!frame && stickyPrompt}
173
- gap={gap}
174
- fontSize={fontSize}
175
- lineHeight={lineHeight}
176
- items={items}
177
- pendingApprovals={state.pendingApprovals}
178
- scrubber={scrubber}
179
- bookmarks={frame ? undefined : bookmarks}
180
- affordances={affordances}
181
- fileUrl={fileUrl}
182
- attachmentUrl={attachmentUrl}
183
- hostImage={hostImage}
184
- jumpToRecapRef={jumpToRecapRef}
185
- repinRef={repinRef}
186
- reveal={frame ? undefined : reveal}
187
- frameParentId={frame?.parentToolUseId}
188
- onOpenSubagent={frame ? undefined : onOpenSubagent}
189
- />
190
- )}
191
- {(frame ? frameTask !== undefined && taskBusy(frameTask, items) : showLoader(state)) ? (
192
- terminal ? (
193
- <>
194
- {state.items.length > 0 ? <div className="term-blank" aria-hidden /> : null}
195
- <WorkingRow
196
- label={state.status === 'starting' ? 'Starting…' : 'Working…'}
197
- startedAt={runStartedAt}
198
- tokens={state.contextUsage?.totalTokens}
147
+ <SessionNamesProvider names={sessionNames}>
148
+ <Conversation className={cn(replaying && 'invisible', className)}>
149
+ <ConversationContent className={cn(terminal && 'gap-0 p-0')}>
150
+ <TerminalShell active={terminal} fontSize={fontSize} lineHeight={lineHeight} affordances={affordances}>
151
+ {frame ? (
152
+ frameTask === undefined && !replaying ? (
153
+ <div className={cn(terminal ? 'term-row text-fg-4' : 'p-4 text-body-sm text-fg-4')}>
154
+ This sub-agent's work is not in this transcript.
155
+ </div>
156
+ ) : null
157
+ ) : items.length === 0 && state.status !== 'starting' ? (
158
+ emptyState !== undefined ? (
159
+ emptyState
160
+ ) : (
161
+ <SessionEmptyState
162
+ cwd={state.cwd}
163
+ hasCommands={!!state.commands?.length}
164
+ hasSkills={!!state.skills?.some((s) => s.enabled)}
165
+ canBrowseFiles={canBrowseFiles}
199
166
  />
200
- </>
201
- ) : (
202
- <Loader label={state.status === 'starting' ? 'Starting session…' : undefined} />
203
- )
204
- ) : null}
205
- </TerminalShell>
206
- </ConversationContent>
207
- <ConversationScrollButton />
208
- {replaying ? (
209
- <div
210
- data-slot="transcript-hold"
211
- aria-hidden
212
- className="wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden"
213
- >
214
- <div className={cn('mx-auto w-full max-w-[var(--wd-transcript-max-width)]', !terminal && 'px-4 py-4')}>
215
- {terminal ? (
216
- <TerminalSurface fontSize={fontSize} lineHeight={lineHeight} affordances={false} bleed="1ch" className="term-transcript">
217
- <WorkingRow label="Loading…" />
218
- </TerminalSurface>
219
- ) : (
220
- <Loader label="Loading session…" />
167
+ )
168
+ ) : null}
169
+ {frame && frameTask === undefined && !replaying ? null : (
170
+ <TranscriptRows
171
+ rows={rows}
172
+ boundary={boundary}
173
+ since={catchUp?.since}
174
+ terminal={terminal}
175
+ replaying={replaying}
176
+ stickyPrompt={!frame && stickyPrompt}
177
+ gap={gap}
178
+ fontSize={fontSize}
179
+ lineHeight={lineHeight}
180
+ items={items}
181
+ pendingApprovals={state.pendingApprovals}
182
+ scrubber={scrubber}
183
+ bookmarks={frame ? undefined : bookmarks}
184
+ affordances={affordances}
185
+ fileUrl={fileUrl}
186
+ attachmentUrl={attachmentUrl}
187
+ hostImage={hostImage}
188
+ jumpToRecapRef={jumpToRecapRef}
189
+ repinRef={repinRef}
190
+ reveal={frame ? undefined : reveal}
191
+ frameParentId={frame?.parentToolUseId}
192
+ onOpenSubagent={frame ? undefined : onOpenSubagent}
193
+ />
221
194
  )}
195
+ {(frame ? frameTask !== undefined && taskBusy(frameTask, items) : showLoader(state)) ? (
196
+ terminal ? (
197
+ <>
198
+ {state.items.length > 0 ? <div className="term-blank" aria-hidden /> : null}
199
+ <WorkingRow
200
+ label={state.status === 'starting' ? 'Starting…' : 'Working…'}
201
+ startedAt={runStartedAt}
202
+ tokens={state.contextUsage?.totalTokens}
203
+ />
204
+ </>
205
+ ) : (
206
+ <Loader label={state.status === 'starting' ? 'Starting session…' : undefined} />
207
+ )
208
+ ) : null}
209
+ </TerminalShell>
210
+ </ConversationContent>
211
+ <ConversationScrollButton />
212
+ {replaying ? (
213
+ <div
214
+ data-slot="transcript-hold"
215
+ aria-hidden
216
+ className="wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden"
217
+ >
218
+ <div className={cn('mx-auto w-full max-w-[var(--wd-transcript-max-width)]', !terminal && 'px-4 py-4')}>
219
+ {terminal ? (
220
+ <TerminalSurface
221
+ fontSize={fontSize}
222
+ lineHeight={lineHeight}
223
+ affordances={false}
224
+ bleed="1ch"
225
+ className="term-transcript"
226
+ >
227
+ <WorkingRow label="Loading…" />
228
+ </TerminalSurface>
229
+ ) : (
230
+ <Loader label="Loading session…" />
231
+ )}
232
+ </div>
222
233
  </div>
223
- </div>
224
- ) : null}
225
- </Conversation>
234
+ ) : null}
235
+ </Conversation>
236
+ </SessionNamesProvider>
226
237
  </SkillNamesProvider>
227
238
  </TranscriptVariantProvider>
228
239
  )
@@ -109,6 +109,24 @@ export function rankComposerRows(query: string, rows: readonly ComposerRow[]): C
109
109
  return scored.map(({ row }) => row)
110
110
  }
111
111
 
112
+ // The `#` picker's ranking. Peers arrive most-recently-active first, and an empty query keeps that
113
+ // order: which session moved last is the most useful thing the list can say before anything is typed.
114
+ export function rankPeerSessions<T extends { slug: string; label: string }>(query: string, peers: readonly T[]): T[] {
115
+ const needle = query.trim()
116
+ if (!needle) {
117
+ return [...peers]
118
+ }
119
+ const scored: Array<{ score: number; index: number; peer: T }> = []
120
+ peers.forEach((peer, index) => {
121
+ const score = matchScore(needle, [peer.slug, peer.label, ...peer.slug.split('-')])
122
+ if (score > 0) {
123
+ scored.push({ score, index, peer })
124
+ }
125
+ })
126
+ scored.sort((a, b) => b.score - a.score || a.index - b.index)
127
+ return scored.map(({ peer }) => peer)
128
+ }
129
+
112
130
  export function matchClientCommand(text: string, commands: readonly ClientCommand[]): { command: ClientCommand; args: string } | undefined {
113
131
  const match = /^\/([\w:-]+)[ \t]*([\s\S]*)$/.exec(text.trim())
114
132
  if (!match) {
package/src/index.ts CHANGED
@@ -94,6 +94,7 @@ export { Composer, type ComposerFileMatch, type ComposerHandle, type ComposerPro
94
94
  export {
95
95
  mergeComposerRows,
96
96
  rankComposerRows,
97
+ rankPeerSessions,
97
98
  matchClientCommand,
98
99
  skillPrompt,
99
100
  type ClientCommand,
@@ -124,11 +125,18 @@ export { SessionBrowser, rowShapeClass, type SessionBrowserProps } from './compo
124
125
  export { SessionItem, type SelectModifiers, type SessionItemProps } from './components/agent/SessionItem.tsx'
125
126
  export { SessionStatusIcon } from './components/agent/SessionStatusIcon.tsx'
126
127
  export { EngineIcon, engineMark, vendorMarkClass, vendorTextClass } from './components/agent/EngineIcon.tsx'
127
- export { type Step, StepRow, StepToggle, runningSteps, sessionSteps } from './components/agent/SessionSteps.tsx'
128
+ export { type Step, StepRow, sessionSteps } from './components/agent/SessionSteps.tsx'
129
+ export {
130
+ SUBAGENT_DISPLAY_META,
131
+ SUBAGENT_DISPLAY_ORDER,
132
+ SubagentDisplayIcon,
133
+ SubagentToggle,
134
+ type SubagentToggleProps,
135
+ } from './components/agent/SubagentToggle.tsx'
128
136
  export { SubagentStrip } from './components/agent/SubagentStrip.tsx'
129
137
  export { ProjectIcon } from './components/agent/ProjectIcon.tsx'
130
138
  export { SessionEmptyState, type SessionEmptyStateProps } from './components/agent/SessionEmptyState.tsx'
131
- export { PromptTokenText } from './components/agent/PromptTokenText.tsx'
139
+ export { PromptTokenText, SessionNamesProvider, SkillNamesProvider } from './components/agent/PromptTokenText.tsx'
132
140
  export { STATUS_META } from './components/agent/status.ts'
133
141
  export { ContextRing } from './components/agent/ContextRing.tsx'
134
142
 
@@ -699,10 +699,18 @@ button.term-link {
699
699
  /* The head's own strip: the full lane, out of flow, so the head needs no
700
700
  footprint-cancelling margin - sticky confinement clamps the margin box, and
701
701
  a negative margin shrinks it to nothing, letting the head overshoot the
702
- lane's end by its own height (two pinned prompts on screen at once). */
702
+ lane's end by its own height (two pinned prompts on screen at once).
703
+
704
+ The z-index is what makes the head's ground actually cover: the row it
705
+ overlays is a later sibling and carries `term-hoverable`, which is
706
+ `position: relative` for the row actions, so at `z-index: auto` the row's
707
+ text painted OVER the pinned head and the two read as one line of overlap.
708
+ Raising the strip - inside the lane's own stacking context, so it still
709
+ sits under nothing else - puts it back above the row it is there to hide. */
703
710
  [data-terminal] [data-sticky-lane] > [data-sticky-headlane] {
704
711
  position: absolute;
705
712
  inset: 0;
713
+ z-index: 1;
706
714
  pointer-events: none;
707
715
  }
708
716