@workerdeck/ui 3.2.0 → 3.4.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-BNNc8grK.d.mts → SessionPanel-DIi4sEgh.d.mts} +5 -2
- package/build/index.d.mts +24 -5
- package/build/index.mjs +297 -62
- package/build/index.mjs.map +1 -1
- package/build/scoped.css +52 -3
- package/build/{use-path-links-C6Fh5zdx.mjs → use-path-links-WCsysQPy.mjs} +337 -83
- package/build/use-path-links-WCsysQPy.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/Composer.tsx +29 -6
- package/src/components/agent/EffortSelect.tsx +64 -0
- package/src/components/agent/ModelSelect.tsx +1 -1
- package/src/components/agent/SessionBrowser.tsx +106 -17
- package/src/components/agent/SessionFilters.tsx +2 -2
- package/src/components/agent/SessionGroups.tsx +248 -0
- package/src/components/agent/SessionItem.tsx +3 -1
- package/src/components/agent/SessionPanel.tsx +75 -28
- package/src/components/agent/TranscriptItemView.tsx +20 -2
- package/src/components/agent/composer-commands.ts +24 -0
- package/src/components/terminal/affordances.tsx +29 -1
- package/src/components/terminal/height.ts +12 -2
- package/src/components/terminal/items.tsx +35 -3
- package/src/components/terminal/live-tool.ts +71 -0
- package/src/components/terminal/tool-run.ts +6 -3
- package/src/index.ts +8 -1
- package/src/styles/terminal.css +9 -2
- package/src/styles/theme.css +3 -1
- package/build/use-path-links-C6Fh5zdx.mjs.map +0 -1
package/build/workspace.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as SessionPanelProps } from "./SessionPanel-
|
|
1
|
+
import { r as SessionPanelProps } from "./SessionPanel-DIi4sEgh.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 Splitter, N as ShellTerminal, Nn as cn, On as Input, Y as EntryIcon, i as SessionPanel, in as Tip, jn as Button, n as usePathLinks, on as TooltipProvider, t as resolveAgainstCwd, tn as Spinner } from "./use-path-links-WCsysQPy.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.
|
|
3
|
+
"version": "3.4.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.
|
|
43
|
-
"@workerdeck/
|
|
44
|
-
"@workerdeck/
|
|
42
|
+
"@workerdeck/client": "3.4.0",
|
|
43
|
+
"@workerdeck/react": "3.4.0",
|
|
44
|
+
"@workerdeck/protocol": "3.4.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"monaco-editor": "^0.56.0",
|
|
@@ -39,6 +39,8 @@ export type ComposerHandle = {
|
|
|
39
39
|
|
|
40
40
|
export interface ComposerProps {
|
|
41
41
|
onSend: (text: string, attachmentIds: string[]) => void
|
|
42
|
+
// Present while a foreground command the engine can background is running: ⌥↵ sends and moves it to the background.
|
|
43
|
+
onSendNow?: (text: string, attachmentIds: string[]) => void
|
|
42
44
|
onInterrupt: () => void
|
|
43
45
|
busy: boolean
|
|
44
46
|
disabled?: boolean
|
|
@@ -70,6 +72,7 @@ export interface ComposerProps {
|
|
|
70
72
|
|
|
71
73
|
export function Composer({
|
|
72
74
|
onSend,
|
|
75
|
+
onSendNow,
|
|
73
76
|
onInterrupt,
|
|
74
77
|
busy,
|
|
75
78
|
disabled,
|
|
@@ -184,7 +187,7 @@ export function Composer({
|
|
|
184
187
|
rankPeerSessions(query, peers).map((peer) => ({
|
|
185
188
|
value: peer.slug,
|
|
186
189
|
label: peer.label,
|
|
187
|
-
description: [peer.engine, peer.project, peer.status].filter(Boolean).join(' · '),
|
|
190
|
+
description: [peer.gateway, peer.engine, peer.project, peer.status].filter(Boolean).join(' · '),
|
|
188
191
|
icon: <Users className="size-3.5 text-fg-3" />,
|
|
189
192
|
})),
|
|
190
193
|
onSelect: (suggestion) => suggestion.value,
|
|
@@ -214,7 +217,7 @@ export function Composer({
|
|
|
214
217
|
const staged = attachments?.items ?? []
|
|
215
218
|
const canSend = !disabled && (!isEmpty || staged.length > 0) && !attachments?.uploading && !attachments?.hasFailure
|
|
216
219
|
|
|
217
|
-
const submit = () => {
|
|
220
|
+
const submit = (now = false) => {
|
|
218
221
|
if (!canSend) {
|
|
219
222
|
return
|
|
220
223
|
}
|
|
@@ -230,7 +233,8 @@ export function Composer({
|
|
|
230
233
|
focus()
|
|
231
234
|
return
|
|
232
235
|
}
|
|
233
|
-
|
|
236
|
+
const deliver = now && onSendNow ? onSendNow : onSend
|
|
237
|
+
deliver(plainText.trim(), attachments?.readyIds ?? [])
|
|
234
238
|
attachments?.clear()
|
|
235
239
|
draft?.clear()
|
|
236
240
|
clear()
|
|
@@ -275,6 +279,11 @@ export function Composer({
|
|
|
275
279
|
setHelpOpen(true)
|
|
276
280
|
return
|
|
277
281
|
}
|
|
282
|
+
if (e.key === 'Enter' && e.altKey && onSendNow && canSend) {
|
|
283
|
+
e.preventDefault()
|
|
284
|
+
submit(true)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
278
287
|
if (helpOpen) {
|
|
279
288
|
setHelpOpen(false)
|
|
280
289
|
}
|
|
@@ -351,9 +360,16 @@ export function Composer({
|
|
|
351
360
|
</span>
|
|
352
361
|
)
|
|
353
362
|
|
|
363
|
+
const sendNow =
|
|
364
|
+
onSendNow && canSend && !shellMode ? (
|
|
365
|
+
<button type="button" className="term-press term-link text-label" onClick={() => submit(true)}>
|
|
366
|
+
⌥↵ send now - moves the running command to the background
|
|
367
|
+
</button>
|
|
368
|
+
) : null
|
|
369
|
+
|
|
354
370
|
const interrupting = busy && !canSend
|
|
355
371
|
const submitButton = terminal ? (
|
|
356
|
-
<GlyphButton label="Send" disabled={!canSend} onClick={submit} tone={canSend ? 'blue' : undefined}>
|
|
372
|
+
<GlyphButton label="Send" disabled={!canSend} onClick={() => submit()} tone={canSend ? 'blue' : undefined}>
|
|
357
373
|
↵
|
|
358
374
|
</GlyphButton>
|
|
359
375
|
) : interrupting ? (
|
|
@@ -361,7 +377,7 @@ export function Composer({
|
|
|
361
377
|
<Square className="size-3" />
|
|
362
378
|
</Button>
|
|
363
379
|
) : (
|
|
364
|
-
<Button size="icon-sm" aria-label="Send" className="rounded-full" disabled={!canSend} onClick={submit}>
|
|
380
|
+
<Button size="icon-sm" aria-label="Send" className="rounded-full" disabled={!canSend} onClick={() => submit()}>
|
|
365
381
|
<ArrowUp className="size-4" />
|
|
366
382
|
</Button>
|
|
367
383
|
)
|
|
@@ -402,7 +418,7 @@ export function Composer({
|
|
|
402
418
|
triggers={triggers}
|
|
403
419
|
markdown={false}
|
|
404
420
|
normalizeBullets={false}
|
|
405
|
-
onSubmit={submit}
|
|
421
|
+
onSubmit={() => submit()}
|
|
406
422
|
disabled={disabled}
|
|
407
423
|
placeholder={disabled ? 'Session ended' : shellMode ? SHELL_PLACEHOLDER : placeholder}
|
|
408
424
|
minHeight={minHeight}
|
|
@@ -428,6 +444,12 @@ export function Composer({
|
|
|
428
444
|
className={cn('term-composer', shellMode && 'term-composer-shell', disabled && 'opacity-60')}
|
|
429
445
|
>
|
|
430
446
|
<div className="term-composer-body">
|
|
447
|
+
{sendNow ? (
|
|
448
|
+
<div className="term-row">
|
|
449
|
+
<span aria-hidden className="term-gutter" />
|
|
450
|
+
{sendNow}
|
|
451
|
+
</div>
|
|
452
|
+
) : null}
|
|
431
453
|
{stagedStrip}
|
|
432
454
|
<div className="term-row">
|
|
433
455
|
{canAttach && !shellMode ? fileField : null}
|
|
@@ -464,6 +486,7 @@ export function Composer({
|
|
|
464
486
|
return (
|
|
465
487
|
<div data-slot="composer" className={cn('px-[var(--wd-composer-padding)] pb-[var(--wd-composer-padding)]', className)}>
|
|
466
488
|
{helpRow}
|
|
489
|
+
{sendNow ? <div className="mx-auto mb-1 w-full max-w-[var(--wd-transcript-max-width)] text-text-muted">{sendNow}</div> : null}
|
|
467
490
|
<div
|
|
468
491
|
{...dropHandlers}
|
|
469
492
|
className={cn(
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { EngineCapabilities, ModelOption } from '@workerdeck/protocol'
|
|
2
|
+
import { Brain } from 'lucide-react'
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue } from '../ui/Select.tsx'
|
|
4
|
+
import { cn } from '../../lib/utils.ts'
|
|
5
|
+
import { matchModel } from './ModelSelect.tsx'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_VALUE = 'default'
|
|
8
|
+
|
|
9
|
+
export function effortChoices(
|
|
10
|
+
models: readonly ModelOption[],
|
|
11
|
+
model: string | undefined,
|
|
12
|
+
capabilities: Pick<EngineCapabilities, 'reasoningEfforts'>,
|
|
13
|
+
): readonly string[] {
|
|
14
|
+
return matchModel(models, model)?.reasoningEfforts ?? capabilities.reasoningEfforts ?? []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EffortSelectProps {
|
|
18
|
+
efforts: readonly string[]
|
|
19
|
+
effort?: string | null
|
|
20
|
+
onEffortChange: (effort?: string) => void
|
|
21
|
+
variant?: 'toolbar' | 'form'
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function EffortSelect({ efforts, effort, onEffortChange, variant = 'toolbar', disabled, className }: EffortSelectProps) {
|
|
27
|
+
const items = [{ value: DEFAULT_VALUE, label: 'Default' }, ...efforts.map((value) => ({ value, label: value }))]
|
|
28
|
+
return (
|
|
29
|
+
<Select
|
|
30
|
+
items={items}
|
|
31
|
+
value={effort ?? null}
|
|
32
|
+
onValueChange={(value) => {
|
|
33
|
+
if (typeof value !== 'string' || value === effort) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
onEffortChange(value === DEFAULT_VALUE ? undefined : value)
|
|
37
|
+
}}
|
|
38
|
+
disabled={disabled}
|
|
39
|
+
>
|
|
40
|
+
<SelectTrigger
|
|
41
|
+
aria-label="Reasoning effort"
|
|
42
|
+
className={cn(
|
|
43
|
+
variant === 'toolbar' && 'h-6 max-w-32 border-transparent bg-transparent text-fg-3 hover:bg-surface-hover',
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<span className={cn('flex items-center gap-1 truncate', variant === 'toolbar' && 'font-mono text-label')}>
|
|
48
|
+
<Brain className="size-3 shrink-0" />
|
|
49
|
+
<SelectValue placeholder="effort" />
|
|
50
|
+
</span>
|
|
51
|
+
</SelectTrigger>
|
|
52
|
+
<SelectContent className="min-w-40">
|
|
53
|
+
{items.map((item) => (
|
|
54
|
+
<SelectItem key={item.value} value={item.value}>
|
|
55
|
+
<SelectItemText>
|
|
56
|
+
<span className="font-medium">{item.label}</span>
|
|
57
|
+
</SelectItemText>
|
|
58
|
+
{item.value === DEFAULT_VALUE ? <span className="text-label text-fg-4">The model's configured default</span> : null}
|
|
59
|
+
</SelectItem>
|
|
60
|
+
))}
|
|
61
|
+
</SelectContent>
|
|
62
|
+
</Select>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -39,7 +39,7 @@ function optionMatches(option: ModelOption, model: string): boolean {
|
|
|
39
39
|
return token !== '' && token === family(dropVariant(option.value))
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function matchModel(models: ModelOption[], model?: string): ModelOption | undefined {
|
|
42
|
+
export function matchModel(models: readonly ModelOption[], model?: string): ModelOption | undefined {
|
|
43
43
|
if (!model) {
|
|
44
44
|
return undefined
|
|
45
45
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react'
|
|
2
|
-
import type { ReactNode } from 'react'
|
|
3
|
-
import { Eraser, FolderOpen, Layers, Pencil, Search, SearchX, Trash2, X } from 'lucide-react'
|
|
4
|
-
import { clearFilters, filterRows, groupRows, hasFacetFilter, scopeActive, subsetSummary } from '@workerdeck/protocol'
|
|
5
|
-
import type { SessionRow, SessionTask, StepDisplay, SubagentDisplay, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
3
|
+
import { BedDouble, Eraser, FolderOpen, Layers, Pencil, Plus, Search, SearchX, Trash2, X } from 'lucide-react'
|
|
4
|
+
import { clearFilters, filterRows, groupRows, hasFacetFilter, scopeActive, sessionKey, subsetSummary } from '@workerdeck/protocol'
|
|
5
|
+
import type { SessionGroup, SessionRow, SessionTask, StepDisplay, SubagentDisplay, ViewConfig, WorkspaceScope } from '@workerdeck/protocol'
|
|
6
6
|
import { Button } from '../ui/Button.tsx'
|
|
7
7
|
import { Empty } from '../ui/Empty.tsx'
|
|
8
8
|
import { Input } from '../ui/Input.tsx'
|
|
9
9
|
import { ProjectIcon } from './ProjectIcon.tsx'
|
|
10
10
|
import { SessionFilters } from './SessionFilters.tsx'
|
|
11
11
|
import { SessionItem, type SelectModifiers } from './SessionItem.tsx'
|
|
12
|
+
import { CustomGroupHeader, GroupHeading, HeadingAction, NewGroupButton, useGroupDrag } from './SessionGroups.tsx'
|
|
12
13
|
import { cn } from '../../lib/utils.ts'
|
|
13
14
|
|
|
14
15
|
export { SessionStatusIcon } from './SessionStatusIcon.tsx'
|
|
@@ -33,12 +34,15 @@ export interface SessionBrowserProps {
|
|
|
33
34
|
onDelete?: (row: SessionRow) => void
|
|
34
35
|
onRename?: (row: SessionRow, title: string) => void
|
|
35
36
|
onClearContext?: (row: SessionRow) => void
|
|
37
|
+
onSleep?: (row: SessionRow) => void
|
|
36
38
|
onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
|
|
37
39
|
onSelectTask?: (row: SessionRow, task: SessionTask) => void
|
|
38
40
|
onStopTask?: (row: SessionRow, toolUseId: string) => void
|
|
39
41
|
onSelectShell?: (row: SessionRow, shellId: string) => void
|
|
40
42
|
onKillShell?: (row: SessionRow, shellId: string) => void
|
|
41
43
|
onShellAgentWrite?: (row: SessionRow, shellId: string, enabled: boolean) => void
|
|
44
|
+
// Draws a `+` on each project heading: start a session on that gateway, in that project's root.
|
|
45
|
+
onCreateInGroup?: (target: GroupTarget) => void
|
|
42
46
|
emptyState?: ReactNode
|
|
43
47
|
// The facet controls, drawn inline. A host with a header puts `SessionFiltersButton` there instead.
|
|
44
48
|
showControls?: boolean
|
|
@@ -48,6 +52,8 @@ export interface SessionBrowserProps {
|
|
|
48
52
|
className?: string
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
export type GroupTarget = { hostId: string; cwd?: string }
|
|
56
|
+
|
|
51
57
|
export function rowShapeClass(active: boolean): string {
|
|
52
58
|
return cn('px-2 py-1.5 hover:bg-row-hover', active ? 'mr-1 ml-0 rounded-r-md border-l-4 border-l-accent' : 'mx-1 rounded-md')
|
|
53
59
|
}
|
|
@@ -69,12 +75,14 @@ export function SessionBrowser({
|
|
|
69
75
|
onDelete,
|
|
70
76
|
onRename,
|
|
71
77
|
onClearContext,
|
|
78
|
+
onSleep,
|
|
72
79
|
onSelectSubagent,
|
|
73
80
|
onSelectTask,
|
|
74
81
|
onStopTask,
|
|
75
82
|
onSelectShell,
|
|
76
83
|
onKillShell,
|
|
77
84
|
onShellAgentWrite,
|
|
85
|
+
onCreateInGroup,
|
|
78
86
|
emptyState,
|
|
79
87
|
showControls = true,
|
|
80
88
|
showSearch = showControls,
|
|
@@ -82,12 +90,65 @@ export function SessionBrowser({
|
|
|
82
90
|
projectIcons,
|
|
83
91
|
className,
|
|
84
92
|
}: SessionBrowserProps) {
|
|
93
|
+
const rowGateways = useMemo(() => new Set(rows.map((row) => row.hostId)).size, [rows])
|
|
94
|
+
const gateways = gatewayCount ?? rowGateways
|
|
85
95
|
const visible = useMemo(() => filterRows(rows, config, scope), [rows, config, scope])
|
|
86
|
-
const groups = useMemo(() => groupRows(visible, config), [visible, config])
|
|
96
|
+
const groups = useMemo(() => groupRows(visible, config, { gatewayCount: gateways }), [visible, config, gateways])
|
|
87
97
|
const subset = subsetSummary(config, scope, visible.length, rows.length)
|
|
88
|
-
const
|
|
98
|
+
const custom = config.groupBy === 'custom'
|
|
99
|
+
const [editingGroup, setEditingGroup] = useState<string>()
|
|
89
100
|
|
|
90
101
|
const set = (patch: Partial<ViewConfig>) => onConfigChange({ ...config, ...patch })
|
|
102
|
+
const drag = useGroupDrag(config.customGroups ?? [], (customGroups) => set({ customGroups }))
|
|
103
|
+
|
|
104
|
+
const heading = (group: SessionGroup) => {
|
|
105
|
+
if (custom) {
|
|
106
|
+
return (
|
|
107
|
+
<CustomGroupHeader
|
|
108
|
+
group={group}
|
|
109
|
+
editing={group.custom !== undefined && editingGroup === group.custom}
|
|
110
|
+
onEditingChange={(editing) => setEditingGroup(editing ? group.custom : undefined)}
|
|
111
|
+
onRename={(name) => group.custom && drag.rename(group.custom, name)}
|
|
112
|
+
onRemove={() => group.custom && drag.remove(group.custom)}
|
|
113
|
+
dragProps={drag.header(group)}
|
|
114
|
+
/>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
if (config.groupBy === 'none' || !group.label) {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
const project = config.groupBy === 'project'
|
|
121
|
+
const target = project && group.hostId ? { hostId: group.hostId, cwd: group.cwd } : undefined
|
|
122
|
+
return (
|
|
123
|
+
<GroupHeading
|
|
124
|
+
label={
|
|
125
|
+
group.gateway && group.project ? (
|
|
126
|
+
<>
|
|
127
|
+
{group.gateway}
|
|
128
|
+
<span aria-hidden> · </span>
|
|
129
|
+
<span className="text-fg-1">{group.project}</span>
|
|
130
|
+
</>
|
|
131
|
+
) : (
|
|
132
|
+
group.label
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
count={group.rows.length}
|
|
136
|
+
title={project && group.cwd ? group.cwd : undefined}
|
|
137
|
+
leading={
|
|
138
|
+
project ? (
|
|
139
|
+
<ProjectIcon icon={group.rows[0]?.info.project?.icon} src={iconSrcOf(group.rows[0], projectIcons)} name={group.label} />
|
|
140
|
+
) : undefined
|
|
141
|
+
}
|
|
142
|
+
actions={
|
|
143
|
+
target && onCreateInGroup ? (
|
|
144
|
+
<HeadingAction label={`New session in ${group.label}`} onClick={() => onCreateInGroup(target)}>
|
|
145
|
+
<Plus className="size-3" />
|
|
146
|
+
</HeadingAction>
|
|
147
|
+
) : undefined
|
|
148
|
+
}
|
|
149
|
+
/>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
91
152
|
|
|
92
153
|
return (
|
|
93
154
|
<div data-slot="session-browser" className={cn('flex flex-col gap-3', className)}>
|
|
@@ -137,26 +198,29 @@ export function SessionBrowser({
|
|
|
137
198
|
) : (
|
|
138
199
|
<div className="flex flex-col gap-4 px-1">
|
|
139
200
|
{groups.map((group) => (
|
|
140
|
-
<div
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
201
|
+
<div
|
|
202
|
+
key={group.key}
|
|
203
|
+
{...(custom ? drag.container(group) : {})}
|
|
204
|
+
className={cn('flex flex-col gap-1 rounded-md', custom && drag.isOver(`group:${group.key}`) && 'bg-row-hover/50')}
|
|
205
|
+
>
|
|
206
|
+
{heading(group)}
|
|
207
|
+
{custom && group.custom !== undefined && group.rows.length === 0 ? (
|
|
208
|
+
<div className="mx-1 rounded-md border border-dashed border-border px-2 py-2 text-center text-label text-fg-4">
|
|
209
|
+
Drag sessions here
|
|
148
210
|
</div>
|
|
149
211
|
) : null}
|
|
150
212
|
{group.rows.map((row) => (
|
|
151
213
|
<SessionRowItem
|
|
152
|
-
key={
|
|
214
|
+
key={sessionKey(row)}
|
|
215
|
+
dragProps={custom ? drag.session(sessionKey(row), group) : undefined}
|
|
216
|
+
dropTarget={custom && drag.isOver(`row:${sessionKey(row)}`)}
|
|
153
217
|
row={row}
|
|
154
218
|
active={isActive(row)}
|
|
155
219
|
activeSubagentId={activeSubagentId}
|
|
156
220
|
activeShellId={activeShellId}
|
|
157
221
|
actions={rowActions?.(row)}
|
|
158
222
|
now={now}
|
|
159
|
-
showGateway={
|
|
223
|
+
showGateway={gateways > 1 && config.groupBy !== 'project'}
|
|
160
224
|
showProject={config.groupBy !== 'project'}
|
|
161
225
|
subagents={config.subagents}
|
|
162
226
|
shells={config.shells}
|
|
@@ -166,6 +230,7 @@ export function SessionBrowser({
|
|
|
166
230
|
onDelete={onDelete}
|
|
167
231
|
onRename={onRename}
|
|
168
232
|
onClearContext={onClearContext}
|
|
233
|
+
onSleep={onSleep}
|
|
169
234
|
onSelectSubagent={onSelectSubagent}
|
|
170
235
|
onSelectTask={onSelectTask}
|
|
171
236
|
onStopTask={onStopTask}
|
|
@@ -176,6 +241,7 @@ export function SessionBrowser({
|
|
|
176
241
|
))}
|
|
177
242
|
</div>
|
|
178
243
|
))}
|
|
244
|
+
{custom ? <NewGroupButton onClick={() => setEditingGroup(drag.create())} /> : null}
|
|
179
245
|
</div>
|
|
180
246
|
)}
|
|
181
247
|
</div>
|
|
@@ -189,6 +255,8 @@ function iconSrcOf(row: SessionRow | undefined, icons: Record<string, string> |
|
|
|
189
255
|
|
|
190
256
|
interface SessionRowItemProps {
|
|
191
257
|
row: SessionRow
|
|
258
|
+
dragProps?: HTMLAttributes<HTMLDivElement>
|
|
259
|
+
dropTarget?: boolean
|
|
192
260
|
active?: boolean
|
|
193
261
|
actions?: ReactNode
|
|
194
262
|
activeSubagentId?: string
|
|
@@ -204,6 +272,7 @@ interface SessionRowItemProps {
|
|
|
204
272
|
onDelete?: (row: SessionRow) => void
|
|
205
273
|
onRename?: (row: SessionRow, title: string) => void
|
|
206
274
|
onClearContext?: (row: SessionRow) => void
|
|
275
|
+
onSleep?: (row: SessionRow) => void
|
|
207
276
|
onSelectSubagent?: (row: SessionRow, toolUseId: string) => void
|
|
208
277
|
onSelectTask?: (row: SessionRow, task: SessionTask) => void
|
|
209
278
|
onStopTask?: (row: SessionRow, toolUseId: string) => void
|
|
@@ -214,6 +283,8 @@ interface SessionRowItemProps {
|
|
|
214
283
|
|
|
215
284
|
function SessionRowItem({
|
|
216
285
|
row,
|
|
286
|
+
dragProps,
|
|
287
|
+
dropTarget,
|
|
217
288
|
active,
|
|
218
289
|
actions,
|
|
219
290
|
activeSubagentId,
|
|
@@ -229,6 +300,7 @@ function SessionRowItem({
|
|
|
229
300
|
onDelete,
|
|
230
301
|
onRename,
|
|
231
302
|
onClearContext,
|
|
303
|
+
onSleep,
|
|
232
304
|
onSelectSubagent,
|
|
233
305
|
onSelectTask,
|
|
234
306
|
onStopTask,
|
|
@@ -239,7 +311,7 @@ function SessionRowItem({
|
|
|
239
311
|
const { info } = row
|
|
240
312
|
const [editing, setEditing] = useState(false)
|
|
241
313
|
|
|
242
|
-
|
|
314
|
+
const item = (
|
|
243
315
|
<SessionItem
|
|
244
316
|
row={row}
|
|
245
317
|
active={active === true}
|
|
@@ -279,6 +351,15 @@ function SessionRowItem({
|
|
|
279
351
|
<Eraser className="size-3 text-fg-3" />
|
|
280
352
|
</RowAction>
|
|
281
353
|
) : null}
|
|
354
|
+
{onSleep && info.capabilities?.engineSleep && info.status === 'idle' && !info.engineAsleep ? (
|
|
355
|
+
<RowAction
|
|
356
|
+
label="Sleep session"
|
|
357
|
+
title="Stop the engine process to free its memory - the session stays here and wakes on your next message"
|
|
358
|
+
onClick={() => onSleep(row)}
|
|
359
|
+
>
|
|
360
|
+
<BedDouble className="size-3 text-fg-3" />
|
|
361
|
+
</RowAction>
|
|
362
|
+
) : null}
|
|
282
363
|
{onDelete ? (
|
|
283
364
|
<RowAction label="Close session" onClick={() => onDelete(row)}>
|
|
284
365
|
<Trash2 className="size-3 text-fg-3" />
|
|
@@ -289,6 +370,14 @@ function SessionRowItem({
|
|
|
289
370
|
}
|
|
290
371
|
/>
|
|
291
372
|
)
|
|
373
|
+
if (!dragProps) {
|
|
374
|
+
return item
|
|
375
|
+
}
|
|
376
|
+
return (
|
|
377
|
+
<div {...dragProps} className={cn('rounded-md', dropTarget && 'shadow-[inset_0_2px_0_0_var(--color-accent)]')}>
|
|
378
|
+
{item}
|
|
379
|
+
</div>
|
|
380
|
+
)
|
|
292
381
|
}
|
|
293
382
|
|
|
294
383
|
function RowAction({ label, title, onClick, children }: { label: string; title?: string; onClick: () => void; children: ReactNode }) {
|
|
@@ -106,8 +106,8 @@ export function SessionFilters({ config, onConfigChange, rows, scope, gateways:
|
|
|
106
106
|
{ value: 'none', label: 'No grouping' },
|
|
107
107
|
{ value: 'state', label: 'By state' },
|
|
108
108
|
{ value: 'adapter', label: 'By engine' },
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
{ value: 'project', label: gateways.length > 1 ? 'By gateway & project' : 'By project' },
|
|
110
|
+
{ value: 'custom', label: 'Custom' },
|
|
111
111
|
]}
|
|
112
112
|
onChange={(groupBy) => set({ groupBy: groupBy as GroupBy })}
|
|
113
113
|
/>
|