cli-jaw 2.17.63 → 2.17.64
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/dist/src/code-mode/store.js +35 -6
- package/dist/src/code-mode/store.js.map +1 -1
- package/dist/src/routes/code-native.js +11 -2
- package/dist/src/routes/code-native.js.map +1 -1
- package/package.json +1 -1
- package/public/dist/.vite/manifest.json +21 -9
- package/public/dist/assets/CodeCanvas-c_omHvtV.js +3 -0
- package/public/dist/assets/FolderPanel-DOLT_syh.js +3 -0
- package/public/dist/assets/{FolderPanel-BHEYcjea.css → FolderPanel-DgoqN1cX.css} +1 -1
- package/public/dist/assets/icons-QgChTDzZ.js +1 -0
- package/public/dist/assets/manager-Bq8-tr-M.js +5 -0
- package/public/dist/assets/manager-C6UJc985.css +1 -0
- package/public/dist/manager/index.html +2 -2
- package/public/manager/src/App.tsx +3 -3
- package/public/manager/src/AppChrome.tsx +1 -1
- package/public/manager/src/background-tasks/BackgroundTaskMonitorPanel.tsx +41 -7
- package/public/manager/src/background-tasks/background-task-monitor.css +1 -4
- package/public/manager/src/code/CodeSessionList.tsx +95 -35
- package/public/manager/src/code/code-controller-runtime.ts +66 -8
- package/public/manager/src/code/code-controller-types.ts +4 -0
- package/public/manager/src/code/code-session-client.ts +1 -1
- package/public/manager/src/code/code-session-sidebar.css +32 -0
- package/public/manager/src/code/code.css +2 -4
- package/public/manager/src/code/session-order.ts +9 -0
- package/public/manager/src/components/CommandBar.tsx +0 -10
- package/public/manager/src/components/InstanceGroups.tsx +2 -0
- package/public/manager/src/components/InstanceListContent.tsx +2 -0
- package/public/manager/src/components/InstanceRow.tsx +146 -89
- package/public/manager/src/components/SidebarRail.tsx +6 -5
- package/public/manager/src/components/ThemeSwitch.tsx +46 -22
- package/public/manager/src/components/context-menu/ContextMenu.tsx +207 -0
- package/public/manager/src/components/context-menu/context-menu.css +111 -0
- package/public/manager/src/components/context-menu/icons.tsx +48 -0
- package/public/manager/src/folder-panel/FolderContextMenu.tsx +43 -17
- package/public/manager/src/folder-panel/FolderPanel.tsx +9 -1
- package/public/manager/src/folder-panel/FolderPanelOverlays.tsx +2 -0
- package/public/manager/src/folder-panel/FolderTreeRows.tsx +6 -1
- package/public/manager/src/folder-panel/folder-panel.css +0 -33
- package/public/manager/src/folder-panel/use-folder-context-menu.ts +6 -19
- package/public/manager/src/hooks/useDashboardRegistry.ts +24 -1
- package/public/manager/src/jaw-ceo/JawCeoConsolePanels.tsx +71 -9
- package/public/manager/src/jaw-ceo/jaw-ceo-console.css +5 -7
- package/public/manager/src/main.tsx +2 -0
- package/public/manager/src/manager-components.css +1 -34
- package/public/manager/src/manager-desktop-titlebar.css +117 -0
- package/public/manager/src/manager-layout.css +0 -1
- package/public/manager/src/manager-p0-1-1.css +0 -42
- package/public/manager/src/manager-polish.css +0 -88
- package/public/dist/assets/CodeCanvas-CVbdp0S-.js +0 -3
- package/public/dist/assets/FolderPanel-CJvwRwIV.js +0 -3
- package/public/dist/assets/manager-CCXm5Yfe.js +0 -5
- package/public/dist/assets/manager-D9rhKFKc.css +0 -1
|
@@ -21,7 +21,7 @@ import { type ScheduleGroup } from './dashboard-schedule/DashboardScheduleSideba
|
|
|
21
21
|
import { normalizeSidebarModeForBuild, REMINDERS_WORKSPACE_ENABLED } from './dashboard-features';
|
|
22
22
|
import { readInitialSelectedPort, readInitialSidebarMode, readTrayRemindersMode } from './dashboard-url-state';
|
|
23
23
|
import { TrayRemindersApp } from './dashboard-reminders/TrayRemindersApp';
|
|
24
|
-
import { useDashboardRegistry } from './hooks/useDashboardRegistry';
|
|
24
|
+
import { useDashboardRegistry, useFavoriteToggle } from './hooks/useDashboardRegistry';
|
|
25
25
|
import { useDashboardView, hydrateInstanceSettings, createInstanceSettingsNavigation, useSettingsDirtyState } from './hooks/useDashboardView';
|
|
26
26
|
import { useActivityUnread } from './hooks/useActivityUnread';
|
|
27
27
|
import { useTheme } from './hooks/useTheme';
|
|
@@ -470,7 +470,7 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
|
|
|
470
470
|
setTransitionAction(null);
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
|
-
|
|
473
|
+
const toggleFavorite = useFavoriteToggle({ save: registry.save, reload: load });
|
|
474
474
|
const instanceListContent = (
|
|
475
475
|
<InstanceListContent error={error} loading={loading} instances={instances} filtered={filtered}
|
|
476
476
|
selectedInstance={selectedInstance} data={data} lifecycleBusyPort={lifecycleBusyPort}
|
|
@@ -481,7 +481,7 @@ export function App() { if (readTrayRemindersMode(window.location.search) && REM
|
|
|
481
481
|
showSelectedRowActions={view.showSelectedRowActions} profiles={profiles} getLabel={instanceLabel}
|
|
482
482
|
formatUptime={formatUptime} onSelect={handleSelectInstance} onPreview={(instance) => handlePreview(instance, true)}
|
|
483
483
|
onMarkActivitySeen={activityUnread.markPortSeen} onInstanceLabelSave={labelEditor.saveInstanceLabel}
|
|
484
|
-
onLifecycle={(action, instance) => void handleLifecycle(action, instance)} />
|
|
484
|
+
onLifecycle={(action, instance) => void handleLifecycle(action, instance)} onToggleFavorite={toggleFavorite} />
|
|
485
485
|
);
|
|
486
486
|
const dashboardSettingsUi = dashboardSettingsUiFromView(view, theme.theme), titleSupport = summarizeActivityTitleSupport(messageActivity.titleSupportByPort);
|
|
487
487
|
const profileChipStrip = (chipProfiles: DashboardProfile[]) => chipProfiles.length > 0 ? <div className="profile-chip-strip drawer-chip-strip" aria-label="Profile filters">{chipProfiles.map(profile => <ProfileChip key={profile.profileId} profile={profile} active={activeProfileIds.includes(profile.profileId)} count={profileCounts[profile.profileId] || 0} onToggle={toggleProfile} />)}</div> : null;
|
|
@@ -129,7 +129,7 @@ export function AppChrome(props: AppChromeProps) {
|
|
|
129
129
|
<PanelLayoutProvider initialPanelState={props.panelInitialState} onStateChange={props.onPanelStateChange}>
|
|
130
130
|
<ManagerShell
|
|
131
131
|
sidebarCollapsed={props.view.sidebarCollapsed}
|
|
132
|
-
commandBar={<CommandBar query={props.query} loading={props.loading} onQueryChange={props.setQuery} onRefresh={() => void props.load()} onOpenDrawer={() => props.view.setDrawerOpen(true)} theme={props.theme.theme} onThemeChange={props.theme.setTheme}
|
|
132
|
+
commandBar={<CommandBar query={props.query} loading={props.loading} onQueryChange={props.setQuery} onRefresh={() => void props.load()} onOpenDrawer={() => props.view.setDrawerOpen(true)} theme={props.theme.theme} onThemeChange={props.theme.setTheme} />}
|
|
133
133
|
workspace={(
|
|
134
134
|
<SidebarRailRouter sidebarCollapsed={props.view.sidebarCollapsed} activityDockCollapsed={props.view.activityDockCollapsed}
|
|
135
135
|
activityDockHeight={props.view.activityDockHeight} drawerOpen={props.view.drawerOpen} onCloseDrawer={() => props.view.setDrawerOpen(false)}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react';
|
|
2
|
+
import { ContextMenu, useContextMenu, type ContextMenuEntry } from '../components/context-menu/ContextMenu';
|
|
3
|
+
import { CopyGlyph, EyeGlyph, RestartGlyph, StopGlyph } from '../components/context-menu/icons';
|
|
2
4
|
import type { BackgroundTaskRow, BackgroundTaskStatus } from './background-task-client';
|
|
3
5
|
import { countBackgroundTasksByStatus, useBackgroundTasks } from './useBackgroundTasks';
|
|
4
6
|
|
|
@@ -71,23 +73,54 @@ type BackgroundTaskRowProps = {
|
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
function BackgroundTaskItem({ task, expanded, busy, onCancel, onCopy, onRetry, onToggle }: BackgroundTaskRowProps) {
|
|
76
|
+
const menu = useContextMenu();
|
|
74
77
|
const preview = resultPreview(task);
|
|
75
78
|
const isRunning = task.status === 'running';
|
|
76
79
|
const canRetry = task.status !== 'running';
|
|
77
80
|
const note = recoveryNote(task);
|
|
81
|
+
const menuEntries: ContextMenuEntry[] = [
|
|
82
|
+
{ id: 'details', label: expanded ? 'Hide details' : 'Show details', icon: <EyeGlyph />, onSelect: () => onToggle(task.id) },
|
|
83
|
+
{ kind: 'separator', id: 'sep-actions' },
|
|
84
|
+
{ id: 'retry', label: 'Retry', icon: <RestartGlyph />, disabled: !canRetry || busy, onSelect: () => onRetry(task) },
|
|
85
|
+
{ id: 'cancel', label: 'Cancel', icon: <StopGlyph />, disabled: !isRunning || busy, onSelect: () => onCancel(task.id) },
|
|
86
|
+
{ kind: 'separator', id: 'sep-copy' },
|
|
87
|
+
{ id: 'copy-result', label: 'Copy result', icon: <CopyGlyph />, disabled: !task.result || busy, onSelect: () => onCopy(task) },
|
|
88
|
+
];
|
|
78
89
|
return (
|
|
79
|
-
<li className={`code-bg-task-item is-${task.status}`}>
|
|
80
|
-
<button
|
|
90
|
+
<li className={`code-bg-task-item is-${task.status}`} onContextMenu={menu.openAt}>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
className="code-bg-task-main"
|
|
94
|
+
onClick={() => onToggle(task.id)}
|
|
95
|
+
aria-expanded={expanded}
|
|
96
|
+
onKeyDown={event => {
|
|
97
|
+
if (event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10')) menu.openAt(event);
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
81
100
|
<span className={`code-bg-task-status is-${task.status}`}>{STATUS_LABEL[task.status]}</span>
|
|
82
101
|
<span className="code-bg-task-title">{task.id}</span>
|
|
83
102
|
<span className="code-bg-task-meta">{taskSubtitle(task)}</span>
|
|
84
103
|
</button>
|
|
85
|
-
<div className="code-bg-task-actions">
|
|
86
|
-
<button
|
|
87
|
-
|
|
104
|
+
<div className="code-bg-task-actions jaw-row-hover-actions">
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
className="jaw-row-icon-btn"
|
|
108
|
+
onClick={() => onRetry(task)}
|
|
109
|
+
disabled={!canRetry || busy}
|
|
110
|
+
title="Run this background task spec again"
|
|
111
|
+
aria-label={`Retry ${task.id}`}
|
|
112
|
+
>
|
|
113
|
+
<RestartGlyph />
|
|
88
114
|
</button>
|
|
89
|
-
<button
|
|
90
|
-
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
className="jaw-row-icon-btn"
|
|
118
|
+
onClick={() => onCancel(task.id)}
|
|
119
|
+
disabled={!isRunning || busy}
|
|
120
|
+
title="Cancel running background task"
|
|
121
|
+
aria-label={`Cancel ${task.id}`}
|
|
122
|
+
>
|
|
123
|
+
<StopGlyph />
|
|
91
124
|
</button>
|
|
92
125
|
</div>
|
|
93
126
|
{expanded && (
|
|
@@ -122,6 +155,7 @@ function BackgroundTaskItem({ task, expanded, busy, onCancel, onCopy, onRetry, o
|
|
|
122
155
|
) : null}
|
|
123
156
|
</div>
|
|
124
157
|
)}
|
|
158
|
+
<ContextMenu state={menu.state} entries={menuEntries} label={`Background task ${task.id}`} onClose={menu.close} />
|
|
125
159
|
</li>
|
|
126
160
|
);
|
|
127
161
|
}
|
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
.code-bg-task-refresh,
|
|
63
|
-
.code-bg-task-actions button,
|
|
64
63
|
.code-bg-task-result-head button {
|
|
65
64
|
min-height: 24px;
|
|
66
65
|
padding: 0 8px;
|
|
@@ -75,7 +74,6 @@
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
.code-bg-task-refresh:disabled,
|
|
78
|
-
.code-bg-task-actions button:disabled,
|
|
79
77
|
.code-bg-task-result-head button:disabled {
|
|
80
78
|
opacity: 0.45;
|
|
81
79
|
cursor: not-allowed;
|
|
@@ -176,8 +174,7 @@
|
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
.code-bg-task-actions {
|
|
179
|
-
|
|
180
|
-
gap: 5px;
|
|
177
|
+
justify-content: flex-end;
|
|
181
178
|
}
|
|
182
179
|
|
|
183
180
|
.code-bg-task-detail {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|
2
3
|
import type { CodeSessionInfo } from '../../../../src/code-mode/wire';
|
|
3
4
|
import type { CodeControllerModel } from './code-controller-types';
|
|
4
5
|
import { CODE_RUNTIME_LABELS, CODE_SESSION_LABELS, codeCanResume, codeSessionBusy } from './code-types';
|
|
5
6
|
import {
|
|
6
7
|
CODE_ACTIVITY_BUCKET_LABELS, codeSessionAttention, codeSessionAttentionLabel, codeSessionUnread, codeWorkspaceName,
|
|
7
|
-
groupCodeSessions, groupCodeSessionsByActivity, groupCodeSessionsByWorkspace,
|
|
8
|
+
comparePinnedCodeSessions, groupCodeSessions, groupCodeSessionsByActivity, groupCodeSessionsByWorkspace,
|
|
8
9
|
} from './session-order';
|
|
9
10
|
import { DEFAULT_MANAGER_SHORTCUT_KEYMAP, formatShortcut } from '../manager-shortcuts';
|
|
11
|
+
import { ContextMenu, useContextMenu, type ContextMenuEntry } from '../components/context-menu/ContextMenu';
|
|
12
|
+
import { ArchiveGlyph, CopyGlyph, EyeGlyph, PencilGlyph, PinGlyph, PlayGlyph } from '../components/context-menu/icons';
|
|
13
|
+
import { copyText } from '../clipboard/copy-text';
|
|
10
14
|
|
|
11
15
|
const NEW_SESSION_SHORTCUT = DEFAULT_MANAGER_SHORTCUT_KEYMAP.newCodeSession;
|
|
12
16
|
|
|
@@ -43,16 +47,22 @@ function SessionRow({ session: s, controller: c, view }: { session: CodeSessionI
|
|
|
43
47
|
const [title, setTitle] = useState(s.title ?? '');
|
|
44
48
|
const [pending, setPending] = useState(false);
|
|
45
49
|
const [error, setError] = useState<string | null>(null);
|
|
50
|
+
const menu = useContextMenu();
|
|
46
51
|
const guard = useRef(false);
|
|
47
52
|
const selectButton = useRef<HTMLButtonElement>(null);
|
|
48
53
|
const errorId = useId();
|
|
49
54
|
const active = c.selectedId === s.sessionId;
|
|
50
55
|
const busy = codeSessionBusy(s);
|
|
56
|
+
const archived = s.archivedAt !== null;
|
|
57
|
+
const pinned = s.pinnedAt !== null;
|
|
51
58
|
const count = active && c.synced ? c.permissions.length : s.pendingPermissionCount;
|
|
52
59
|
const attention = codeSessionAttention(count);
|
|
53
|
-
|
|
60
|
+
// An archived row never lights the completion-based unread rule, but an
|
|
61
|
+
// explicit mark still counts so it can be toggled back off.
|
|
62
|
+
const unread = !active && (s.markedUnread || codeSessionUnread(s));
|
|
54
63
|
const working = c.workingIds?.has(s.sessionId) ?? false;
|
|
55
64
|
const waiting = working && s.status === 'idle';
|
|
65
|
+
const name = s.title || 'Untitled session';
|
|
56
66
|
async function action(run: () => Promise<void>, after?: () => void) {
|
|
57
67
|
if (guard.current) return;
|
|
58
68
|
guard.current = true; setPending(true); setError(null);
|
|
@@ -61,27 +71,85 @@ function SessionRow({ session: s, controller: c, view }: { session: CodeSessionI
|
|
|
61
71
|
finally { guard.current = false; setPending(false); }
|
|
62
72
|
}
|
|
63
73
|
function finishRename() { setRenaming(false); selectButton.current?.focus(); }
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
async function copy(value: string) {
|
|
75
|
+
const result = await copyText(value);
|
|
76
|
+
if (!result.ok) throw new Error(result.error ?? 'Copy failed.');
|
|
77
|
+
}
|
|
78
|
+
const entries: ContextMenuEntry[] = [
|
|
79
|
+
{ id: 'rename', label: 'Rename', icon: <PencilGlyph />, disabled: pending || busy,
|
|
80
|
+
...(busy ? { title: 'Stop before renaming' } : {}),
|
|
81
|
+
onSelect: () => { setTitle(s.title ?? ''); setRenaming(true); } },
|
|
82
|
+
{ id: 'pin', label: pinned ? 'Unpin' : 'Pin', icon: <PinGlyph />, disabled: pending,
|
|
83
|
+
onSelect: () => void action(() => c.pin(s.sessionId, !pinned)) },
|
|
84
|
+
{ id: 'unread', label: unread ? 'Mark as read' : 'Mark as unread', icon: <EyeGlyph />, disabled: pending || active,
|
|
85
|
+
...(active ? { title: 'The open session is already read' } : {}),
|
|
86
|
+
onSelect: () => void action(() => c.markUnread(s.sessionId, !unread)) },
|
|
87
|
+
{ id: 'archive', label: archived ? 'Restore' : 'Archive', icon: <ArchiveGlyph />, disabled: pending || busy,
|
|
88
|
+
...(busy ? { title: 'Stop before archiving' } : {}),
|
|
89
|
+
onSelect: () => void action(() => c.archive(s.sessionId, !archived)) },
|
|
90
|
+
...(codeCanResume(s) ? [
|
|
91
|
+
{ kind: 'separator', id: 'resume-sep' } as const,
|
|
92
|
+
{ id: 'resume', label: 'Resume', icon: <PlayGlyph />, disabled: pending || !active || !c.synced || c.pending,
|
|
93
|
+
title: !active ? 'Select this session to resume it' : 'Resume without resending a prompt',
|
|
94
|
+
onSelect: () => void action(c.resume) },
|
|
95
|
+
] : []),
|
|
96
|
+
{ kind: 'separator', id: 'copy-sep' },
|
|
97
|
+
{ id: 'copy-id', label: 'Copy session ID', icon: <CopyGlyph />,
|
|
98
|
+
onSelect: () => void action(() => copy(s.sessionId)) },
|
|
99
|
+
{ id: 'copy-cwd', label: 'Copy working directory', icon: <CopyGlyph />,
|
|
100
|
+
onSelect: () => void action(() => copy(s.cwd)) },
|
|
101
|
+
];
|
|
102
|
+
function openMenuFromKey(event: ReactKeyboardEvent<HTMLElement>) {
|
|
103
|
+
if (event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10')) menu.openAt(event);
|
|
104
|
+
}
|
|
105
|
+
return <li className="code-session-row" onContextMenu={event => {
|
|
106
|
+
if ((event.target as Element).closest('.code-session-rename')) return;
|
|
107
|
+
menu.openAt(event);
|
|
108
|
+
}}>
|
|
109
|
+
<div className="code-session-item-wrap" onKeyDown={openMenuFromKey}>
|
|
110
|
+
<button ref={selectButton} type="button" className={`code-session-item${active ? ' active' : ''}`}
|
|
111
|
+
aria-current={active ? 'true' : undefined}
|
|
112
|
+
onClick={() => void action(() => c.selectSession(s.sessionId))}>
|
|
113
|
+
<span className="code-session-cwd">
|
|
114
|
+
{unread && <span className="code-session-unread-dot" role="img" aria-label="Unread" />}
|
|
115
|
+
{name}
|
|
116
|
+
</span>
|
|
117
|
+
{/* Projects view already names the workspace in the group heading. */}
|
|
118
|
+
<span className="code-session-meta" title={s.cwd}>{view === 'activity'
|
|
119
|
+
? `${codeWorkspaceName(s.cwd)} · ${CODE_RUNTIME_LABELS[s.provider]}` : CODE_RUNTIME_LABELS[s.provider]}</span>
|
|
120
|
+
{/* Ready is the common case and stays silent. A status on every row
|
|
121
|
+
is a status on no row, and it costs the one session that is
|
|
122
|
+
actually doing something its visibility. */}
|
|
123
|
+
<span className={`code-session-status code-session-status-${waiting ? 'starting' : s.status}`}
|
|
124
|
+
data-quiet={s.status === 'idle' && !working ? 'true' : undefined}>
|
|
125
|
+
{working && <span className="code-session-spinner" aria-hidden="true" />}
|
|
126
|
+
{waiting ? CODE_SESSION_LABELS.starting : CODE_SESSION_LABELS[s.status]}</span>
|
|
127
|
+
<span className={`code-session-attention code-session-attention-${attention.kind}`}
|
|
128
|
+
data-quiet={attention.kind === 'none' ? 'true' : undefined}>{codeSessionAttentionLabel(attention)}</span>
|
|
129
|
+
</button>
|
|
130
|
+
{/* Row actions replace the status text on hover or keyboard focus,
|
|
131
|
+
Codex-style; opacity keeps them tabbable instead of display:none. */}
|
|
132
|
+
<span className="code-session-hover-actions">
|
|
133
|
+
<button type="button" className={`code-session-hover-btn${pinned ? ' is-on' : ''}`}
|
|
134
|
+
aria-label={pinned ? `Unpin ${name}` : `Pin ${name}`} aria-pressed={pinned}
|
|
135
|
+
title={pinned ? 'Unpin' : 'Pin'} disabled={pending}
|
|
136
|
+
onClick={event => { event.stopPropagation(); void action(() => c.pin(s.sessionId, !pinned)); }}>
|
|
137
|
+
<PinGlyph />
|
|
138
|
+
</button>
|
|
139
|
+
<button type="button" className="code-session-hover-btn"
|
|
140
|
+
aria-label={archived ? `Restore ${name}` : `Archive ${name}`}
|
|
141
|
+
title={busy ? 'Stop before archiving' : archived ? 'Restore' : 'Archive'} disabled={pending || busy}
|
|
142
|
+
onClick={event => { event.stopPropagation(); void action(() => c.archive(s.sessionId, !archived)); }}>
|
|
143
|
+
<ArchiveGlyph />
|
|
144
|
+
</button>
|
|
145
|
+
<button type="button" className="code-session-hover-btn"
|
|
146
|
+
aria-label={`More actions for ${name}`} aria-haspopup="menu" title="More actions"
|
|
147
|
+
onClick={event => { event.stopPropagation(); menu.openAtElement(event.currentTarget); }}>
|
|
148
|
+
<span aria-hidden="true">⋯</span>
|
|
149
|
+
</button>
|
|
70
150
|
</span>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
? `${codeWorkspaceName(s.cwd)} · ${CODE_RUNTIME_LABELS[s.provider]}` : CODE_RUNTIME_LABELS[s.provider]}</span>
|
|
74
|
-
{/* Ready is the common case and stays silent. A status on every row
|
|
75
|
-
is a status on no row, and it costs the one session that is
|
|
76
|
-
actually doing something its visibility. */}
|
|
77
|
-
<span className={`code-session-status code-session-status-${waiting ? 'starting' : s.status}`}
|
|
78
|
-
data-quiet={s.status === 'idle' && !working ? 'true' : undefined}>
|
|
79
|
-
{working && <span className="code-session-spinner" aria-hidden="true" />}
|
|
80
|
-
{waiting ? CODE_SESSION_LABELS.starting : CODE_SESSION_LABELS[s.status]}</span>
|
|
81
|
-
<span className={`code-session-attention code-session-attention-${attention.kind}`}
|
|
82
|
-
data-quiet={attention.kind === 'none' ? 'true' : undefined}>{codeSessionAttentionLabel(attention)}</span>
|
|
83
|
-
</button>
|
|
84
|
-
{renaming ? <form className="code-session-rename" onSubmit={event => {
|
|
151
|
+
</div>
|
|
152
|
+
{renaming && <form className="code-session-rename" onSubmit={event => {
|
|
85
153
|
event.preventDefault(); if (title.trim()) void action(() => c.rename(s.sessionId, title.trim()), finishRename);
|
|
86
154
|
}}>
|
|
87
155
|
<input autoFocus aria-label="Session title" aria-describedby={error ? errorId : undefined} value={title} disabled={pending}
|
|
@@ -90,18 +158,8 @@ function SessionRow({ session: s, controller: c, view }: { session: CodeSessionI
|
|
|
90
158
|
}} />
|
|
91
159
|
<button type="submit" disabled={pending || !title.trim()}>Save</button>
|
|
92
160
|
<button type="button" disabled={pending} onClick={finishRename}>Cancel</button>
|
|
93
|
-
</form>
|
|
94
|
-
|
|
95
|
-
<div>
|
|
96
|
-
<button type="button" disabled={pending || busy} onClick={() => { setTitle(s.title ?? ''); setRenaming(true); }}>Rename</button>
|
|
97
|
-
<button type="button" disabled={pending || busy} title={busy ? 'Stop before archiving' : undefined}
|
|
98
|
-
onClick={() => void action(() => c.archive(s.sessionId, s.archivedAt === null))}>{s.archivedAt === null ? 'Archive' : 'Restore'}</button>
|
|
99
|
-
{codeCanResume(s) && <button type="button" disabled={pending || !active || !c.synced || c.pending}
|
|
100
|
-
title={!active ? 'Select this session to resume it' : 'Resume without resending a prompt'}
|
|
101
|
-
onClick={() => void action(c.resume)}>Resume</button>}
|
|
102
|
-
{busy && <small>Stop before changing session metadata.</small>}
|
|
103
|
-
</div>
|
|
104
|
-
</details>}
|
|
161
|
+
</form>}
|
|
162
|
+
<ContextMenu state={menu.state} entries={entries} label={`Actions for ${name}`} onClose={menu.close} />
|
|
105
163
|
{pending && <span className="code-session-view-hint" role="status">Updating session…</span>}
|
|
106
164
|
{error && <div id={errorId} className="code-session-list-error" role="alert">{error}</div>}
|
|
107
165
|
</li>;
|
|
@@ -131,12 +189,14 @@ export function CodeSessionList({ controller: c, newSessionShortcut = NEW_SESSIO
|
|
|
131
189
|
// answering a prompt never moves a row under the reader's cursor. Activity is the
|
|
132
190
|
// reader's explicit choice: unread first, then last activity by day.
|
|
133
191
|
const anyUnread = c.sessions.some(row => row.sessionId !== c.selectedId && codeSessionUnread(row));
|
|
134
|
-
const
|
|
192
|
+
const pinned = visible.filter(row => row.archivedAt === null && row.pinnedAt !== null).sort(comparePinnedCodeSessions);
|
|
193
|
+
const live = visible.filter(row => row.archivedAt === null && row.pinnedAt === null);
|
|
135
194
|
const archived = visible.filter(row => row.archivedAt !== null);
|
|
136
195
|
const groups: { key: string; title: string; cwd: string | null; sessions: CodeSessionInfo[] }[] = view === 'activity'
|
|
137
196
|
? groupCodeSessionsByActivity(live, Date.now(), c.selectedId).map(group => ({
|
|
138
197
|
key: group.bucket, title: CODE_ACTIVITY_BUCKET_LABELS[group.bucket], cwd: null, sessions: group.sessions }))
|
|
139
198
|
: groupCodeSessionsByWorkspace(live).map(group => ({ key: group.cwd, title: group.cwd, cwd: group.cwd, sessions: group.sessions }));
|
|
199
|
+
if (pinned.length) groups.unshift({ key: 'pinned', title: 'Pinned', cwd: null, sessions: pinned });
|
|
140
200
|
if (archived.length) groups.push({ key: 'archived', title: 'Archived', cwd: null, sessions: groupCodeSessions(archived).flatMap(group => group.sessions) });
|
|
141
201
|
// The chord resolves through the manager shortcut system (rebindable in
|
|
142
202
|
// Settings); the runner re-broadcasts it here so Code mode can be absent
|
|
@@ -87,6 +87,7 @@ export class CodeController {
|
|
|
87
87
|
private book: CodeDraftBook;
|
|
88
88
|
private details = new Map<string, CodeSessionState>();
|
|
89
89
|
private summaries = new Map<string, CodeSessionInfo>();
|
|
90
|
+
private pinnedOverlay: CodeSessionInfo[] = [];
|
|
90
91
|
private attention = new Map<string, CodeSessionAttention>();
|
|
91
92
|
private rows: string[] = [];
|
|
92
93
|
private catalog: CodeModelCatalog | null = null;
|
|
@@ -162,6 +163,7 @@ export class CodeController {
|
|
|
162
163
|
this.book.listeners.delete(this.changed);
|
|
163
164
|
this.details.clear();
|
|
164
165
|
this.summaries.clear();
|
|
166
|
+
this.pinnedOverlay = [];
|
|
165
167
|
this.attention.clear();
|
|
166
168
|
this.rows = [];
|
|
167
169
|
this.cursor = null;
|
|
@@ -196,7 +198,9 @@ export class CodeController {
|
|
|
196
198
|
if (!id) return null;
|
|
197
199
|
const detail = this.details.get(id)?.session;
|
|
198
200
|
const summary = this.summaries.get(id);
|
|
199
|
-
|
|
201
|
+
const overlay = this.pinnedOverlay.find(row => row.sessionId === id);
|
|
202
|
+
return [detail, summary, overlay].reduce<CodeSessionInfo | null>((best, row) =>
|
|
203
|
+
row && newer(row, best) ? row : best, null);
|
|
200
204
|
}
|
|
201
205
|
/**
|
|
202
206
|
* Record what an owning read saw. Call this BEFORE accept() on the same object:
|
|
@@ -236,9 +240,15 @@ export class CodeController {
|
|
|
236
240
|
// observe(), and update() hands over details.session, which the reducer may
|
|
237
241
|
// still be holding (code-session-state.ts snapshot replace).
|
|
238
242
|
const { contextUsage: _usage, pendingPermissionCount: _count, ...session } = incoming;
|
|
243
|
+
const overlayIndex = this.pinnedOverlay.findIndex(row => row.sessionId === session.sessionId);
|
|
244
|
+
if (overlayIndex !== -1) {
|
|
245
|
+
if (session.pinnedAt === null || session.archivedAt !== null) this.pinnedOverlay.splice(overlayIndex, 1);
|
|
246
|
+
else this.pinnedOverlay[overlayIndex] = session;
|
|
247
|
+
}
|
|
239
248
|
this.summaries.set(session.sessionId, session);
|
|
240
249
|
if (this.summaries.size > MAX_INDEX + MAX_DETAILS) {
|
|
241
|
-
const victim = [...this.summaries.keys()].find(id => id !== this.book.selectedId && !this.details.has(id)
|
|
250
|
+
const victim = [...this.summaries.keys()].find(id => id !== this.book.selectedId && !this.details.has(id)
|
|
251
|
+
&& !this.rows.includes(id) && !this.pinnedOverlay.some(row => row.sessionId === id));
|
|
242
252
|
if (victim) { this.summaries.delete(victim); this.attention.delete(victim); }
|
|
243
253
|
}
|
|
244
254
|
const draft = this.book.sessions.get(session.sessionId);
|
|
@@ -376,7 +386,7 @@ export class CodeController {
|
|
|
376
386
|
&& item.itemId.startsWith(`${session.turnId}:steer:`));
|
|
377
387
|
const unconfirmed = draft.steer?.state !== 'unknown' ? null : draft.steer.unrecorded ? FOLLOW_UP_UNRECORDED : FOLLOW_UP_UNCONFIRMED;
|
|
378
388
|
return {
|
|
379
|
-
catalog: this.catalog, sessions: this.rows.map(id => {
|
|
389
|
+
catalog: this.catalog, sessions: [...this.rows, ...this.pinnedOverlay.map(row => row.sessionId).filter(id => !this.rows.includes(id))].map(id => {
|
|
380
390
|
const row = this.info(id), detail = this.details.get(id);
|
|
381
391
|
return row ? publish(row, detail) : row;
|
|
382
392
|
}).filter((row): row is CodeSessionInfo => !!row),
|
|
@@ -386,7 +396,7 @@ export class CodeController {
|
|
|
386
396
|
input: draft.input, selection: session ? sessionSelection(session) : draft.selection,
|
|
387
397
|
gitInfo: this.gitInfo, loading: this.indexLoading || !!detail?.hydrating || (!!id && !detail?.hydrated && !detail?.error),
|
|
388
398
|
pending, busy: codeSessionBusy(session), working: !!draft.awaitingTurn || codeSessionBusy(session),
|
|
389
|
-
workingIds: new Set(this.rows.filter(rowId => {
|
|
399
|
+
workingIds: new Set([...this.rows, ...this.pinnedOverlay.map(row => row.sessionId)].filter(rowId => {
|
|
390
400
|
const row = this.info(rowId);
|
|
391
401
|
return !!this.book.sessions.get(rowId)?.awaitingTurn || (!!row && codeSessionBusy(row));
|
|
392
402
|
})), followUp, followUpSent, steering: draft.steer?.state === 'sending', synced, transport: this.transport, workspacePicking: this.workspacePicking,
|
|
@@ -400,7 +410,7 @@ export class CodeController {
|
|
|
400
410
|
newSession: this.newSession, selectSession: this.selectSession, setInput: this.setInput,
|
|
401
411
|
setSelection: this.setSelection, pickWorkspace: this.pickWorkspace, send: this.send,
|
|
402
412
|
retrySameSend: this.retrySameSend, stop: this.stop, resume: this.resume, rollbackSession: this.rollbackSession, rename: this.rename,
|
|
403
|
-
archive: this.archive, answer: this.answer, refresh: this.refresh, loadMoreSessions: this.loadMoreSessions,
|
|
413
|
+
archive: this.archive, pin: this.pin, markUnread: this.markUnread, answer: this.answer, refresh: this.refresh, loadMoreSessions: this.loadMoreSessions,
|
|
404
414
|
loadOlderHistory: this.loadOlderHistory, setFilter: this.setFilter, clearError: this.clearError,
|
|
405
415
|
};
|
|
406
416
|
}
|
|
@@ -434,7 +444,13 @@ export class CodeController {
|
|
|
434
444
|
const page = await this.client.listSessions({ ...this.filter, limit,
|
|
435
445
|
...(cursor === null ? {} : { cursor }),
|
|
436
446
|
...(this.filter.scope === 'cwd' ? { cwd: this.workingDir } : {}) }, this.abort.signal);
|
|
447
|
+
const overlay = more || this.filter.archived ? null : await this.client.listSessions({
|
|
448
|
+
...(this.filter.scope === 'cwd' ? { cwd: this.workingDir } : {}),
|
|
449
|
+
archived: false, pinned: true, limit: MAX_INDEX,
|
|
450
|
+
}, this.abort.signal).catch(() => null);
|
|
437
451
|
if (!this.active || life !== this.lifetime || generation !== this.indexGeneration) return;
|
|
452
|
+
if (!more && !overlay) this.pinnedOverlay = [];
|
|
453
|
+
this.rows = [...new Set([...(more ? this.rows : []), ...page.sessions.map(row => row.sessionId)])].slice(0, MAX_INDEX);
|
|
438
454
|
for (const session of page.sessions) {
|
|
439
455
|
// A listing is one of the two reads that can see the live runtime.
|
|
440
456
|
this.observe(session);
|
|
@@ -445,12 +461,23 @@ export class CodeController {
|
|
|
445
461
|
void this.sync(session.sessionId);
|
|
446
462
|
}
|
|
447
463
|
}
|
|
448
|
-
|
|
464
|
+
if (overlay) {
|
|
465
|
+
const retained: CodeSessionInfo[] = [];
|
|
466
|
+
for (const remote of overlay.sessions) {
|
|
467
|
+
const local = this.info(remote.sessionId);
|
|
468
|
+
const session = local && !newer(remote, local) ? local : remote;
|
|
469
|
+
if (session.pinnedAt === null || session.archivedAt !== null) continue;
|
|
470
|
+
retained.push(session);
|
|
471
|
+
if (session === remote) { this.observe(session); this.accept(session); }
|
|
472
|
+
}
|
|
473
|
+
this.pinnedOverlay = retained;
|
|
474
|
+
}
|
|
449
475
|
this.cursor = page.nextCursor;
|
|
450
476
|
this.fetched = (more ? this.fetched : 0) + page.sessions.length;
|
|
451
477
|
this.moreSessions = page.hasMore && this.rows.length < MAX_INDEX;
|
|
452
478
|
this.indexError = null;
|
|
453
|
-
const retained = new Set([...this.rows, ...this.
|
|
479
|
+
const retained = new Set([...this.rows, ...this.pinnedOverlay.map(row => row.sessionId),
|
|
480
|
+
...this.details.keys(), ...(this.book.selectedId ? [this.book.selectedId] : [])]);
|
|
454
481
|
for (const id of this.summaries.keys()) if (!retained.has(id)) { this.summaries.delete(id); this.attention.delete(id); }
|
|
455
482
|
} catch (error) {
|
|
456
483
|
if (this.active && life === this.lifetime && generation === this.indexGeneration) this.indexError = message(error);
|
|
@@ -557,7 +584,7 @@ export class CodeController {
|
|
|
557
584
|
await Promise.all([this.readCatalog(), this.readIndex(), id ? this.sync(id, true) : Promise.resolve(), this.readGit()]);
|
|
558
585
|
};
|
|
559
586
|
loadMoreSessions = async (): Promise<void> => { if (this.moreSessions && !this.indexLoading) await this.readIndex(true); };
|
|
560
|
-
setFilter = (filter: CodeSessionFilter): void => { this.filter = { ...filter }; this.cursor = null; this.fetched = 0; this.rows = []; this.moreSessions = false; this.notify(); void this.readIndex(); };
|
|
587
|
+
setFilter = (filter: CodeSessionFilter): void => { this.filter = { ...filter }; this.cursor = null; this.fetched = 0; this.rows = []; this.pinnedOverlay = []; this.moreSessions = false; this.notify(); void this.readIndex(); };
|
|
561
588
|
// Explicitly abandon only the local uncertain attempt. The server session may exist.
|
|
562
589
|
startAnotherSession = (): void => {
|
|
563
590
|
const previous = this.book.fresh;
|
|
@@ -704,6 +731,37 @@ export class CodeController {
|
|
|
704
731
|
// Other actions report handled failures through the controller model.
|
|
705
732
|
rename = async (id: string, title: string): Promise<void> => { await this.patch(id, { title }, true); };
|
|
706
733
|
archive = async (id: string, archived: boolean): Promise<void> => { await this.patch(id, { archived }, true); };
|
|
734
|
+
// Sidebar metadata is allowed while a session is busy, but it still shares
|
|
735
|
+
// the per-session operation slot: two PATCHes would race on the same
|
|
736
|
+
// expectedRevision and one would lose.
|
|
737
|
+
private async sidebarPatch(id: string, input: { pinned?: boolean; unread?: boolean }): Promise<void> {
|
|
738
|
+
const draft = this.draft(id);
|
|
739
|
+
const session = this.info(id);
|
|
740
|
+
if (!session || draft.operation.kind !== 'idle') {
|
|
741
|
+
const error = new Error(!session ? 'Refresh this session before changing it.'
|
|
742
|
+
: 'Wait for the current session action to finish before changing it.');
|
|
743
|
+
draft.operation.error = error.message; this.notify();
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
let failure: Error | null = null;
|
|
747
|
+
draft.operation = { kind: 'patching', error: null }; this.notify();
|
|
748
|
+
try {
|
|
749
|
+
this.accept(await this.client.patchSession(id, { ...input, expectedRevision: session.revision }));
|
|
750
|
+
draft.operation = { kind: 'idle', error: null };
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (error instanceof CodeClientError && error.session?.sessionId === id) {
|
|
753
|
+
// A revision conflict answers with the snapshot session, overlay included.
|
|
754
|
+
this.observe(error.session);
|
|
755
|
+
this.accept(error.session);
|
|
756
|
+
}
|
|
757
|
+
failure = error instanceof CodeClientError ? error : new Error(message(error));
|
|
758
|
+
draft.operation = { kind: 'idle', error: failure.message };
|
|
759
|
+
}
|
|
760
|
+
this.notify(); this.scheduleIndex();
|
|
761
|
+
if (failure) throw failure;
|
|
762
|
+
}
|
|
763
|
+
pin = async (id: string, pinned: boolean): Promise<void> => { await this.sidebarPatch(id, { pinned }); };
|
|
764
|
+
markUnread = async (id: string, unread: boolean): Promise<void> => { await this.sidebarPatch(id, { unread }); };
|
|
707
765
|
send = async (): Promise<void> => {
|
|
708
766
|
let id = this.book.selectedId;
|
|
709
767
|
let draft = this.draft(id);
|
|
@@ -64,6 +64,10 @@ export interface CodeControllerModel {
|
|
|
64
64
|
rollbackSession(itemId: string): Promise<void>;
|
|
65
65
|
rename(id: string, title: string): Promise<void>;
|
|
66
66
|
archive(id: string, archived: boolean): Promise<void>;
|
|
67
|
+
/** Sidebar pin/unpin; allowed while the session is busy. */
|
|
68
|
+
pin(id: string, pinned: boolean): Promise<void>;
|
|
69
|
+
/** Sidebar mark unread/read; allowed while the session is busy. */
|
|
70
|
+
markUnread(id: string, unread: boolean): Promise<void>;
|
|
67
71
|
answer(permission: CodePermissionRequest, optionId: string): Promise<void>;
|
|
68
72
|
refresh(): Promise<void>;
|
|
69
73
|
loadMoreSessions(): Promise<void>;
|
|
@@ -29,7 +29,7 @@ export interface CodeGitInfo {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface CodeListOptions {
|
|
32
|
-
scope?: 'all' | 'cwd'; cwd?: string; archived?: boolean; cursor?: string; limit?: number;
|
|
32
|
+
scope?: 'all' | 'cwd'; cwd?: string; archived?: boolean; pinned?: boolean; cursor?: string; limit?: number;
|
|
33
33
|
}
|
|
34
34
|
export interface CodeSessionClient {
|
|
35
35
|
listSessions(options?: CodeListOptions, signal?: AbortSignal): Promise<CodeSessionPage>;
|
|
@@ -36,3 +36,35 @@
|
|
|
36
36
|
display: flex; align-items: center; gap: 6px; padding: 8px 12px 12px;
|
|
37
37
|
color: var(--text-dim); font-size: 12px;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/* Row hover actions: pin + archive replace the status text at the right edge,
|
|
41
|
+
Codex-style. Opacity (not display) keeps them in the tab order. */
|
|
42
|
+
.code-session-item-wrap { position: relative; }
|
|
43
|
+
.code-session-hover-actions {
|
|
44
|
+
position: absolute; top: 50%; right: 6px; transform: translateY(-50%);
|
|
45
|
+
display: inline-flex; align-items: center; gap: 2px;
|
|
46
|
+
opacity: 0; pointer-events: none;
|
|
47
|
+
}
|
|
48
|
+
.code-session-row:hover .code-session-hover-actions,
|
|
49
|
+
.code-session-row:focus-within .code-session-hover-actions { opacity: 1; pointer-events: auto; }
|
|
50
|
+
@media (hover: none) {
|
|
51
|
+
.code-session-hover-actions { opacity: 1; pointer-events: auto; }
|
|
52
|
+
}
|
|
53
|
+
.code-session-hover-btn {
|
|
54
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
55
|
+
width: 22px; height: 22px; min-height: 22px; padding: 0;
|
|
56
|
+
border: none; border-radius: 5px;
|
|
57
|
+
background: transparent; color: var(--text-dim); cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
.code-session-hover-btn:hover:not(:disabled) { background: var(--accent-soft); color: var(--accent); }
|
|
60
|
+
.code-session-hover-btn.is-on { color: var(--accent); }
|
|
61
|
+
.code-session-row:hover .code-session-item,
|
|
62
|
+
.code-session-row:focus-within .code-session-item { padding-right: 78px; }
|
|
63
|
+
@media (hover: none) {
|
|
64
|
+
.code-session-item { padding-right: 78px; }
|
|
65
|
+
.code-session-status { visibility: hidden; }
|
|
66
|
+
}
|
|
67
|
+
/* Only the status text yields to the hover icons; a pending-approval marker
|
|
68
|
+
stays on screen — hovering to pin must not hide the thing asking for input. */
|
|
69
|
+
.code-session-row:hover .code-session-status,
|
|
70
|
+
.code-session-row:focus-within .code-session-status { visibility: hidden; }
|
|
@@ -775,9 +775,7 @@
|
|
|
775
775
|
white-space: nowrap;
|
|
776
776
|
}
|
|
777
777
|
.code-session-row { margin-bottom: 6px; }
|
|
778
|
-
.code-session-
|
|
779
|
-
.code-session-actions summary { cursor: pointer; min-height: 24px; }
|
|
780
|
-
.code-session-actions > div, .code-session-rename { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 0; }
|
|
778
|
+
.code-session-rename { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 0; }
|
|
781
779
|
.code-session-rename input { min-width: 0; width: 100%; padding: 5px; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--bg-base); color: var(--text-primary); font: inherit; }
|
|
782
780
|
.code-session-archive-filter { display: flex; align-items: center; gap: 5px; margin: 4px 8px 8px; color: var(--text-secondary); font-size: 11px; }
|
|
783
781
|
.code-session-search { box-sizing: border-box; }
|
|
@@ -792,7 +790,7 @@
|
|
|
792
790
|
.code-message-text { overflow-wrap: anywhere; }
|
|
793
791
|
.code-transcript-controls { display: flex; flex-wrap: wrap; gap: 8px; padding: 4px 16px; }
|
|
794
792
|
.code-transcript-controls:empty { display: none; }
|
|
795
|
-
.code-transcript-controls button, .code-inline-action,
|
|
793
|
+
.code-transcript-controls button, .code-inline-action,
|
|
796
794
|
.code-session-rename button, .code-input-recovery button, .code-transport-status button,
|
|
797
795
|
.code-recovery-strip button { min-height: 26px; padding: 3px 8px; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--bg-panel); color: var(--text-primary); font: inherit; font-size: 11px; cursor: pointer; }
|
|
798
796
|
.code-canvas button:disabled, .code-session-list button:disabled { opacity: .6; cursor: not-allowed; }
|
|
@@ -24,6 +24,14 @@ export function compareCodeSessions(left: CodeSessionInfo, right: CodeSessionInf
|
|
|
24
24
|
return right.createdAt - left.createdAt || left.sessionId.localeCompare(right.sessionId);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Pinned rows leave their workspace or activity group and sit together on top,
|
|
29
|
+
* most recently pinned first. `null` sorts below any real pin.
|
|
30
|
+
*/
|
|
31
|
+
export function comparePinnedCodeSessions(left: CodeSessionInfo, right: CodeSessionInfo): number {
|
|
32
|
+
return (right.pinnedAt ?? 0) - (left.pinnedAt ?? 0) || compareCodeSessions(left, right);
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
export type CodeSessionGroup = {
|
|
28
36
|
section: CodeSessionSection;
|
|
29
37
|
sessions: CodeSessionInfo[];
|
|
@@ -77,6 +85,7 @@ export function codeSessionAttentionLabel(attention: CodeSessionAttention): stri
|
|
|
77
85
|
*/
|
|
78
86
|
export function codeSessionUnread(session: CodeSessionInfo): boolean {
|
|
79
87
|
if (session.archivedAt !== null) return false;
|
|
88
|
+
if (session.markedUnread) return true;
|
|
80
89
|
const completed = session.lastTurnCompletedAt;
|
|
81
90
|
const visited = session.lastVisitedAt;
|
|
82
91
|
if (completed === null || visited === null) return false;
|
|
@@ -12,7 +12,6 @@ type CommandBarProps = {
|
|
|
12
12
|
onRefresh: () => void;
|
|
13
13
|
onOpenDrawer: () => void;
|
|
14
14
|
onThemeChange: (next: DashboardUiTheme) => void;
|
|
15
|
-
onOpenPalette: () => void;
|
|
16
15
|
};
|
|
17
16
|
|
|
18
17
|
export function CommandBar(props: CommandBarProps) {
|
|
@@ -47,15 +46,6 @@ export function CommandBar(props: CommandBarProps) {
|
|
|
47
46
|
<div className="command-actions-group">
|
|
48
47
|
<DesktopPanelControls />
|
|
49
48
|
<DesktopLink />
|
|
50
|
-
<button
|
|
51
|
-
type="button"
|
|
52
|
-
className="command-palette-trigger"
|
|
53
|
-
onClick={props.onOpenPalette}
|
|
54
|
-
aria-label="Open command palette"
|
|
55
|
-
title="Open command palette (⌘K / Ctrl+K)"
|
|
56
|
-
>
|
|
57
|
-
<span aria-hidden="true">⌘K</span>
|
|
58
|
-
</button>
|
|
59
49
|
<ThemeSwitch theme={props.theme} onChange={props.onThemeChange} />
|
|
60
50
|
<button
|
|
61
51
|
type="button"
|
|
@@ -43,6 +43,7 @@ type InstanceGroupsProps = {
|
|
|
43
43
|
onPreview: (instance: DashboardInstance) => void;
|
|
44
44
|
onMarkActivitySeen: (port: number) => void;
|
|
45
45
|
onInstanceLabelSave: (port: number, label: string | null) => Promise<void>;
|
|
46
|
+
onToggleFavorite?: (instance: DashboardInstance) => void;
|
|
46
47
|
onLifecycle: (action: DashboardLifecycleAction, instance: DashboardInstance) => void;
|
|
47
48
|
};
|
|
48
49
|
|
|
@@ -139,6 +140,7 @@ function renderInstanceRow(
|
|
|
139
140
|
onPreview={props.onPreview}
|
|
140
141
|
onMarkActivitySeen={props.onMarkActivitySeen}
|
|
141
142
|
onInstanceLabelSave={props.onInstanceLabelSave}
|
|
143
|
+
{...(props.onToggleFavorite !== undefined ? { onToggleFavorite: props.onToggleFavorite } : {})}
|
|
142
144
|
onLifecycle={props.onLifecycle}
|
|
143
145
|
jumpHint={jumpHint ?? null}
|
|
144
146
|
/>
|