@saadnvd1/agent-os 0.3.768 → 0.3.775
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/app/api/sessions/[id]/artifacts/[artifactId]/compare/route.ts +41 -0
- package/app/api/sessions/resolve/route.test.ts +81 -0
- package/app/api/sessions/resolve/route.ts +14 -0
- package/app/api/shares/preview/route.ts +6 -1
- package/app/api/shares/route.test.ts +51 -0
- package/app/page.tsx +1 -0
- package/components/Artifacts/ArtifactActions.tsx +27 -3
- package/components/Artifacts/ArtifactPreview.tsx +27 -12
- package/components/Artifacts/Compare/DiffView.tsx +116 -0
- package/components/Artifacts/Compare/SideBySide.tsx +60 -0
- package/components/Artifacts/Compare/VersionPicker.tsx +47 -0
- package/components/Artifacts/Compare/index.tsx +191 -0
- package/components/Chat/ChatPanel.tsx +3 -9
- package/components/Chat/EventRow.tsx +4 -4
- package/components/Chat/Messages.tsx +12 -6
- package/components/Chat/SessionAnchor.tsx +22 -0
- package/components/Chat/ShareFromHere.tsx +39 -0
- package/components/Chat/session-link.ts +21 -4
- package/components/CommandPalette/CommandPalette.tsx +5 -4
- package/components/CommandPalette/CommandsBody.tsx +97 -131
- package/components/CommandPalette/PaletteRows.tsx +79 -0
- package/components/CommandPalette/PaletteTabs.tsx +51 -0
- package/components/CommandPalette/conversations/ConversationSearch.tsx +13 -11
- package/components/CommandPalette/useAppCommands.ts +14 -0
- package/components/CommandPalette/useConversationRows.ts +55 -0
- package/components/Health/EnvFailures/SessionList.tsx +2 -2
- package/components/Pane/PaneMenu.tsx +1 -1
- package/components/SessionList/QueuedRow.tsx +4 -4
- package/components/SessionList/SessionRowMenu.tsx +1 -1
- package/components/SessionList/useSessionSwitching.ts +2 -2
- package/components/Share/MessageList.tsx +87 -0
- package/components/Share/RangePicker.tsx +59 -0
- package/components/Share/ShareDialog.tsx +113 -52
- package/components/Shares/SharedLinksPanel.tsx +3 -5
- package/data/session-artifacts/keys.ts +2 -0
- package/data/session-artifacts/queries.ts +25 -0
- package/data/shares/queries.ts +11 -0
- package/hooks/useCopyToClipboard.ts +5 -2
- package/hooks/usePaletteCommands.ts +4 -1
- package/hooks/useSessionHref.ts +20 -0
- package/hooks/useSessionUrl.ts +40 -11
- package/lib/artifacts/actions.ts +11 -1
- package/lib/artifacts/compare/compareVersions.ts +66 -0
- package/lib/artifacts/compare/diff.ts +178 -0
- package/lib/artifacts/compare/extractText.ts +64 -0
- package/lib/artifacts/compare/index.ts +11 -0
- package/lib/artifacts/compare/types.ts +44 -0
- package/lib/artifacts/match.ts +64 -0
- package/lib/artifacts/store.ts +39 -10
- package/lib/artifacts/tools.ts +9 -4
- package/lib/artifacts/versions.ts +2 -1
- package/lib/away/deploy.ts +39 -2
- package/lib/palette/registry.ts +71 -17
- package/lib/palette/sections.ts +143 -0
- package/lib/palette/tabs.ts +21 -0
- package/lib/palette/usage.ts +81 -0
- package/lib/reports/evidence/build.ts +4 -3
- package/lib/security/demo.ts +3 -0
- package/lib/session-audit/render.ts +2 -1
- package/lib/session-logs/gather.ts +2 -2
- package/lib/session-slug.ts +29 -0
- package/lib/session-url.ts +162 -40
- package/lib/sessions/resolve.ts +55 -0
- package/lib/settings-url.ts +1 -1
- package/lib/share/markdown.ts +3 -0
- package/lib/share/range.ts +189 -0
- package/lib/share/render.tsx +3 -0
- package/lib/share/service.ts +64 -19
- package/lib/share/snapshot.ts +39 -2
- package/lib/share/styles.ts +2 -0
- package/lib/share/view.ts +11 -0
- package/lib/welcome-url.ts +1 -1
- package/next.config.ts +4 -0
- package/package.json +1 -1
- package/stores/chatJump.ts +5 -1
- package/stores/palette.ts +13 -11
- package/stores/shareUi.ts +6 -1
|
@@ -2,84 +2,84 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useSnapshot } from "valtio";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { ArrowRight, Search } from "lucide-react";
|
|
6
|
+
import { type PaletteCommand, type PaletteTab } from "@/lib/palette/registry";
|
|
7
|
+
import {
|
|
8
|
+
allSections,
|
|
9
|
+
emptySections,
|
|
10
|
+
withSeeAll,
|
|
11
|
+
tabItems,
|
|
12
|
+
type Section,
|
|
13
|
+
} from "@/lib/palette/sections";
|
|
14
|
+
import { cycleTab, parsePrefix } from "@/lib/palette/tabs";
|
|
15
|
+
import { loadUsage, noteUse } from "@/lib/palette/usage";
|
|
10
16
|
import { usePaletteList } from "@/hooks/usePaletteCommands";
|
|
11
|
-
import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
12
17
|
import { paletteActions, paletteUi } from "@/stores/palette";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
18
|
+
import { PaletteRows } from "./PaletteRows";
|
|
19
|
+
import { PaletteTabs } from "./PaletteTabs";
|
|
20
|
+
import { useConversationRows } from "./useConversationRows";
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const PLACEHOLDER: Record<PaletteTab, string> = {
|
|
23
|
+
all: "Search everything…",
|
|
24
|
+
sessions: "Search sessions…",
|
|
25
|
+
commands: "Search commands…",
|
|
26
|
+
settings: "Search settings…",
|
|
27
|
+
conversations: "",
|
|
28
|
+
};
|
|
29
|
+
// A tab's rows are cut here, so a long list stays quick to draw.
|
|
30
|
+
const TAB_LIMIT = 100;
|
|
20
31
|
|
|
21
32
|
export function CommandsBody() {
|
|
22
33
|
const commands = usePaletteList();
|
|
23
34
|
// Subscribed for changes, read raw: its items are held by ref.
|
|
24
35
|
useSnapshot(paletteUi);
|
|
25
36
|
const picker = paletteUi.picker;
|
|
37
|
+
const tab = paletteUi.tab;
|
|
26
38
|
const all = picker?.items ?? commands;
|
|
27
39
|
const [query, setQuery] = useState(paletteUi.query);
|
|
28
40
|
const [active, setActive] = useState(0);
|
|
41
|
+
// The tab a `>` or `@` chose, which Backspace on an empty query undoes.
|
|
42
|
+
const [prefixTab, setPrefixTab] = useState<PaletteTab | null>(null);
|
|
43
|
+
const [usage, setUsage] = useState(loadUsage);
|
|
44
|
+
const [now] = useState(Date.now);
|
|
29
45
|
const listRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const q = query.trim();
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
const { workspace } = useSelectedWorkspace();
|
|
33
|
-
const debounced = useDebouncedValue(query.trim(), 150);
|
|
34
|
-
const openHit = useOpenHit();
|
|
35
|
-
const { data: found } = useConversationSearch(
|
|
36
|
-
debounced,
|
|
37
|
-
workspace?.id ?? "all",
|
|
38
|
-
{
|
|
39
|
-
limit: CONVERSATIONS,
|
|
40
|
-
enabled: !picker,
|
|
41
|
-
}
|
|
42
|
-
);
|
|
43
|
-
const conversations = useMemo<PaletteCommand[]>(() => {
|
|
44
|
-
const q = query.trim();
|
|
45
|
-
if (picker || q.length < MIN_QUERY || q !== debounced) return [];
|
|
46
|
-
const hits = (found?.groups ?? []).flatMap((group) =>
|
|
47
|
-
group.hits.slice(0, 1).map((hit) => ({
|
|
48
|
-
id: `conversation:${group.sessionId}:${hit.itemId}`,
|
|
49
|
-
title: group.name,
|
|
50
|
-
detail: plain(hit.snippet),
|
|
51
|
-
group: "Conversations",
|
|
52
|
-
icon: MessagesSquare,
|
|
53
|
-
run: () => openHit(group, hit),
|
|
54
|
-
keepOpen: true,
|
|
55
|
-
}))
|
|
56
|
-
);
|
|
57
|
-
return [
|
|
58
|
-
...hits,
|
|
59
|
-
{
|
|
60
|
-
id: "conversation:all",
|
|
61
|
-
title: `Search all conversations for “${q}”`,
|
|
62
|
-
group: "Conversations",
|
|
63
|
-
icon: Search,
|
|
64
|
-
hint: "tab",
|
|
65
|
-
keepOpen: true,
|
|
66
|
-
run: () => paletteActions.setMode("conversations", q),
|
|
67
|
-
},
|
|
68
|
-
];
|
|
69
|
-
}, [found, query, debounced, picker, openHit]);
|
|
48
|
+
const conversations = useConversationRows(query, !picker && tab === "all");
|
|
70
49
|
|
|
71
|
-
const
|
|
72
|
-
if (query.trim()) return [...matchCommands(all, query), ...conversations];
|
|
50
|
+
const sections = useMemo<Section[]>(() => {
|
|
73
51
|
// A picker shows every choice; it's the whole list.
|
|
74
|
-
if (picker) return all;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
52
|
+
if (picker) return [{ name: "", items: all }];
|
|
53
|
+
if (tab === "all") {
|
|
54
|
+
if (!q) return emptySections(all, usage);
|
|
55
|
+
const found = withSeeAll(
|
|
56
|
+
allSections(all, query, usage, now),
|
|
57
|
+
(to) => paletteActions.setTab(to, query),
|
|
58
|
+
ArrowRight
|
|
59
|
+
);
|
|
60
|
+
return conversations.length
|
|
61
|
+
? [...found, { name: "Conversations", items: conversations }]
|
|
62
|
+
: found;
|
|
63
|
+
}
|
|
64
|
+
const items = tabItems(all, tab, query, usage, now).slice(0, TAB_LIMIT);
|
|
65
|
+
if (q) return [{ name: "", items }];
|
|
66
|
+
// Without a query, a long tab reads better under its own headings.
|
|
67
|
+
const groups: Section[] = [];
|
|
68
|
+
for (const c of items) {
|
|
69
|
+
let g = groups.find((x) => x.name === c.group);
|
|
70
|
+
if (!g) groups.push((g = { name: c.group, items: [] }));
|
|
71
|
+
g.items.push(c);
|
|
72
|
+
}
|
|
73
|
+
return groups;
|
|
74
|
+
}, [all, tab, q, query, usage, now, picker, conversations]);
|
|
75
|
+
const results = useMemo(() => sections.flatMap((s) => s.items), [sections]);
|
|
76
|
+
|
|
77
|
+
// A new tab starts at its top.
|
|
78
|
+
const [shownTab, setShownTab] = useState(tab);
|
|
79
|
+
if (shownTab !== tab) {
|
|
80
|
+
setShownTab(tab);
|
|
81
|
+
setActive(0);
|
|
82
|
+
}
|
|
83
83
|
const highlighted = Math.min(active, Math.max(results.length - 1, 0));
|
|
84
84
|
|
|
85
85
|
useEffect(() => {
|
|
@@ -88,23 +88,29 @@ export function CommandsBody() {
|
|
|
88
88
|
?.scrollIntoView({ block: "nearest" });
|
|
89
89
|
}, [highlighted]);
|
|
90
90
|
|
|
91
|
+
const goTo = (next: PaletteTab) => {
|
|
92
|
+
setPrefixTab(null);
|
|
93
|
+
paletteActions.setTab(next, query);
|
|
94
|
+
};
|
|
95
|
+
|
|
91
96
|
const run = (c: PaletteCommand | undefined) => {
|
|
92
97
|
if (!c) return;
|
|
93
98
|
if (c.keepOpen) return c.run();
|
|
99
|
+
if (!picker && !c.id.startsWith("conversation:")) setUsage(noteUse(c.id));
|
|
94
100
|
paletteActions.setOpen(false);
|
|
95
101
|
// After the dialog lets go of focus, so what it opens can take it.
|
|
96
102
|
setTimeout(() => c.run(), 0);
|
|
97
103
|
};
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
105
|
+
const onChange = (value: string) => {
|
|
106
|
+
const prefix = !picker && !query ? parsePrefix(value) : null;
|
|
107
|
+
setActive(0);
|
|
108
|
+
if (prefix) {
|
|
109
|
+
setQuery(prefix.text);
|
|
110
|
+
setPrefixTab(prefix.tab);
|
|
111
|
+
paletteActions.setTab(prefix.tab, prefix.text);
|
|
112
|
+
} else setQuery(value);
|
|
113
|
+
};
|
|
108
114
|
|
|
109
115
|
return (
|
|
110
116
|
<>
|
|
@@ -113,10 +119,7 @@ export function CommandsBody() {
|
|
|
113
119
|
<input
|
|
114
120
|
autoFocus
|
|
115
121
|
value={query}
|
|
116
|
-
onChange={(e) =>
|
|
117
|
-
setQuery(e.target.value);
|
|
118
|
-
setActive(0);
|
|
119
|
-
}}
|
|
122
|
+
onChange={(e) => onChange(e.target.value)}
|
|
120
123
|
onKeyDown={(e) => {
|
|
121
124
|
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
122
125
|
e.preventDefault();
|
|
@@ -126,10 +129,18 @@ export function CommandsBody() {
|
|
|
126
129
|
);
|
|
127
130
|
} else if (e.key === "Tab" && !picker) {
|
|
128
131
|
e.preventDefault();
|
|
129
|
-
|
|
132
|
+
goTo(cycleTab(tab, e.shiftKey ? -1 : 1));
|
|
130
133
|
} else if (e.key === "Enter") {
|
|
131
134
|
e.preventDefault();
|
|
132
135
|
run(results[highlighted]);
|
|
136
|
+
} else if (
|
|
137
|
+
e.key === "Backspace" &&
|
|
138
|
+
!query &&
|
|
139
|
+
prefixTab &&
|
|
140
|
+
tab === prefixTab
|
|
141
|
+
) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
goTo("all");
|
|
133
144
|
} else if (
|
|
134
145
|
picker &&
|
|
135
146
|
(e.metaKey || e.ctrlKey) &&
|
|
@@ -139,21 +150,19 @@ export function CommandsBody() {
|
|
|
139
150
|
run(picker.items[Number(e.key) - 1]);
|
|
140
151
|
}
|
|
141
152
|
}}
|
|
142
|
-
placeholder={
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
aria-label={
|
|
146
|
-
picker?.placeholder ?? "Search actions, sessions and conversations"
|
|
147
|
-
}
|
|
153
|
+
placeholder={picker?.placeholder ?? PLACEHOLDER[tab]}
|
|
154
|
+
aria-label={picker?.placeholder ?? PLACEHOLDER[tab].replace("…", "")}
|
|
148
155
|
className="placeholder:text-muted-foreground h-12 min-w-0 flex-1 bg-transparent text-base outline-none sm:text-sm"
|
|
149
156
|
/>
|
|
150
157
|
<kbd className="text-muted-foreground hidden font-mono text-[10px] sm:inline">
|
|
151
158
|
esc
|
|
152
159
|
</kbd>
|
|
153
160
|
</div>
|
|
161
|
+
{!picker && <PaletteTabs tab={tab} onSelect={goTo} />}
|
|
154
162
|
<div
|
|
155
163
|
ref={listRef}
|
|
156
164
|
role="listbox"
|
|
165
|
+
aria-labelledby={picker ? undefined : `palette-tab-${tab}`}
|
|
157
166
|
className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-1.5"
|
|
158
167
|
>
|
|
159
168
|
{results.length === 0 && (
|
|
@@ -161,55 +170,12 @@ export function CommandsBody() {
|
|
|
161
170
|
Nothing matches.
|
|
162
171
|
</p>
|
|
163
172
|
)}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
)}
|
|
171
|
-
{g.items.map(({ c, i }) => {
|
|
172
|
-
const Icon = c.icon as LucideIcon | undefined;
|
|
173
|
-
return (
|
|
174
|
-
<button
|
|
175
|
-
key={c.id}
|
|
176
|
-
type="button"
|
|
177
|
-
role="option"
|
|
178
|
-
aria-selected={i === highlighted}
|
|
179
|
-
data-index={i}
|
|
180
|
-
onMouseMove={() => i !== highlighted && setActive(i)}
|
|
181
|
-
onClick={() => run(c)}
|
|
182
|
-
className={cn(
|
|
183
|
-
"flex min-h-11 w-full items-center gap-2.5 rounded-lg px-2 text-left text-sm md:min-h-9",
|
|
184
|
-
i === highlighted && "bg-primary/10"
|
|
185
|
-
)}
|
|
186
|
-
>
|
|
187
|
-
{Icon && (
|
|
188
|
-
<Icon className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
189
|
-
)}
|
|
190
|
-
<span className="min-w-0 flex-1">
|
|
191
|
-
<span className="block truncate">{c.title}</span>
|
|
192
|
-
{c.detail && (
|
|
193
|
-
<span className="text-muted-foreground block truncate text-xs">
|
|
194
|
-
{c.detail}
|
|
195
|
-
</span>
|
|
196
|
-
)}
|
|
197
|
-
</span>
|
|
198
|
-
{query.trim() && c.group !== "Conversations" && (
|
|
199
|
-
<span className="text-muted-foreground shrink-0 text-xs">
|
|
200
|
-
{c.group}
|
|
201
|
-
</span>
|
|
202
|
-
)}
|
|
203
|
-
{c.hint && (
|
|
204
|
-
<kbd className="text-muted-foreground hidden shrink-0 font-mono text-[11px] sm:inline">
|
|
205
|
-
{c.hint}
|
|
206
|
-
</kbd>
|
|
207
|
-
)}
|
|
208
|
-
</button>
|
|
209
|
-
);
|
|
210
|
-
})}
|
|
211
|
-
</div>
|
|
212
|
-
))}
|
|
173
|
+
<PaletteRows
|
|
174
|
+
sections={sections}
|
|
175
|
+
highlighted={highlighted}
|
|
176
|
+
onHover={setActive}
|
|
177
|
+
onRun={run}
|
|
178
|
+
/>
|
|
213
179
|
</div>
|
|
214
180
|
</>
|
|
215
181
|
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { LucideIcon } from "lucide-react";
|
|
4
|
+
import type { PaletteCommand } from "@/lib/palette/registry";
|
|
5
|
+
import type { Section } from "@/lib/palette/sections";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
// The sections' rows under their headings, numbered in order so the caller's
|
|
9
|
+
// highlight and Enter agree with what's drawn.
|
|
10
|
+
export function PaletteRows({
|
|
11
|
+
sections,
|
|
12
|
+
highlighted,
|
|
13
|
+
onHover,
|
|
14
|
+
onRun,
|
|
15
|
+
}: {
|
|
16
|
+
sections: Section[];
|
|
17
|
+
highlighted: number;
|
|
18
|
+
onHover: (i: number) => void;
|
|
19
|
+
onRun: (c: PaletteCommand) => void;
|
|
20
|
+
}) {
|
|
21
|
+
let index = 0;
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
{sections.map((s) => (
|
|
25
|
+
<div key={s.name || "results"} className="mb-1">
|
|
26
|
+
{s.name && (
|
|
27
|
+
<p className="text-muted-foreground px-2 pt-2 pb-1 text-[11px] font-medium">
|
|
28
|
+
{s.name}
|
|
29
|
+
</p>
|
|
30
|
+
)}
|
|
31
|
+
{s.items.map((c) => {
|
|
32
|
+
const i = index++;
|
|
33
|
+
const Icon = c.icon as LucideIcon | undefined;
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
key={c.id}
|
|
37
|
+
type="button"
|
|
38
|
+
role="option"
|
|
39
|
+
aria-selected={i === highlighted}
|
|
40
|
+
data-index={i}
|
|
41
|
+
onMouseMove={() => i !== highlighted && onHover(i)}
|
|
42
|
+
onClick={() => onRun(c)}
|
|
43
|
+
className={cn(
|
|
44
|
+
"flex min-h-11 w-full items-center gap-2.5 rounded-lg px-2 text-left text-sm md:min-h-9",
|
|
45
|
+
i === highlighted && "bg-primary/10",
|
|
46
|
+
c.id.startsWith("see-all:") && "text-muted-foreground"
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{Icon && (
|
|
50
|
+
<Icon className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
51
|
+
)}
|
|
52
|
+
<span className="min-w-0 flex-1">
|
|
53
|
+
<span className="block truncate">{c.title}</span>
|
|
54
|
+
{c.detail && (
|
|
55
|
+
<span
|
|
56
|
+
className={cn(
|
|
57
|
+
"block truncate text-xs",
|
|
58
|
+
c.status === "waiting"
|
|
59
|
+
? "text-amber-700 dark:text-amber-400"
|
|
60
|
+
: "text-muted-foreground"
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{c.detail}
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
</span>
|
|
67
|
+
{c.hint && (
|
|
68
|
+
<kbd className="text-muted-foreground hidden shrink-0 font-mono text-[11px] sm:inline">
|
|
69
|
+
{c.hint}
|
|
70
|
+
</kbd>
|
|
71
|
+
)}
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { PALETTE_TABS, type PaletteTab } from "@/lib/palette/registry";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
// The palette's tabs. Tab and Shift-Tab in the input move between them, so
|
|
8
|
+
// these keep out of the focus order and never take focus from it.
|
|
9
|
+
export function PaletteTabs({
|
|
10
|
+
tab,
|
|
11
|
+
onSelect,
|
|
12
|
+
}: {
|
|
13
|
+
tab: PaletteTab;
|
|
14
|
+
onSelect: (tab: PaletteTab) => void;
|
|
15
|
+
}) {
|
|
16
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
ref.current
|
|
19
|
+
?.querySelector('[aria-selected="true"]')
|
|
20
|
+
?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
21
|
+
}, [tab]);
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
role="tablist"
|
|
26
|
+
aria-label="Palette sections"
|
|
27
|
+
className="border-foreground/[0.06] flex shrink-0 scrollbar-none gap-1 overflow-x-auto border-b px-2 py-1.5"
|
|
28
|
+
>
|
|
29
|
+
{PALETTE_TABS.map((t) => (
|
|
30
|
+
<button
|
|
31
|
+
key={t.id}
|
|
32
|
+
type="button"
|
|
33
|
+
role="tab"
|
|
34
|
+
id={`palette-tab-${t.id}`}
|
|
35
|
+
aria-selected={t.id === tab}
|
|
36
|
+
tabIndex={-1}
|
|
37
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
38
|
+
onClick={() => onSelect(t.id)}
|
|
39
|
+
className={cn(
|
|
40
|
+
"flex min-h-11 shrink-0 items-center rounded-lg px-3 text-sm whitespace-nowrap md:min-h-8 md:px-2.5 md:text-xs",
|
|
41
|
+
t.id === tab
|
|
42
|
+
? "bg-primary/10 text-foreground font-medium"
|
|
43
|
+
: "text-muted-foreground hover:text-foreground"
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{t.label}
|
|
47
|
+
</button>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -8,14 +8,16 @@ import { MIN_QUERY, useConversationSearch } from "@/data/search";
|
|
|
8
8
|
import { useDebouncedValue } from "@/hooks/useDebouncedValue";
|
|
9
9
|
import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
10
10
|
import { useViewport } from "@/hooks/useViewport";
|
|
11
|
+
import { cycleTab } from "@/lib/palette/tabs";
|
|
11
12
|
import { paletteActions, paletteUi } from "@/stores/palette";
|
|
13
|
+
import { PaletteTabs } from "../PaletteTabs";
|
|
12
14
|
import { NoResults, Recent, Skeleton } from "./EmptyState";
|
|
13
15
|
import { Preview } from "./Preview";
|
|
14
16
|
import { ResultList, flatten } from "./ResultList";
|
|
15
17
|
import { loadRecent, saveRecent, withRecent } from "./recent";
|
|
16
18
|
import { useOpenHit } from "./useOpenHit";
|
|
17
19
|
|
|
18
|
-
// ⌘K's
|
|
20
|
+
// ⌘K's Conversations tab: what was said in any chat, found as you type. Results on
|
|
19
21
|
// the left, the selected message in its conversation on the right; a phone
|
|
20
22
|
// shows the list, and a tap shows the message.
|
|
21
23
|
export function ConversationSearch() {
|
|
@@ -97,7 +99,10 @@ export function ConversationSearch() {
|
|
|
97
99
|
openHit(highlighted);
|
|
98
100
|
} else if (e.key === "Tab") {
|
|
99
101
|
e.preventDefault();
|
|
100
|
-
paletteActions.
|
|
102
|
+
paletteActions.setTab(
|
|
103
|
+
cycleTab("conversations", e.shiftKey ? -1 : 1),
|
|
104
|
+
text
|
|
105
|
+
);
|
|
101
106
|
}
|
|
102
107
|
}}
|
|
103
108
|
placeholder="Search every conversation…"
|
|
@@ -111,8 +116,12 @@ export function ConversationSearch() {
|
|
|
111
116
|
esc
|
|
112
117
|
</kbd>
|
|
113
118
|
</div>
|
|
119
|
+
<PaletteTabs
|
|
120
|
+
tab="conversations"
|
|
121
|
+
onSelect={(tab) => paletteActions.setTab(tab, text)}
|
|
122
|
+
/>
|
|
114
123
|
{workspace && (
|
|
115
|
-
<div className="flex items-center gap-2 px-3
|
|
124
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
116
125
|
<div className="flex overflow-hidden rounded-md border text-xs">
|
|
117
126
|
{(
|
|
118
127
|
[
|
|
@@ -222,19 +231,12 @@ export function ConversationSearch() {
|
|
|
222
231
|
<kbd className="font-mono">↵</kbd> open
|
|
223
232
|
</span>
|
|
224
233
|
<span>
|
|
225
|
-
<kbd className="font-mono">tab</kbd>
|
|
234
|
+
<kbd className="font-mono">tab</kbd> next tab
|
|
226
235
|
</span>
|
|
227
236
|
<span>
|
|
228
237
|
<kbd className="font-mono">esc</kbd> close
|
|
229
238
|
</span>
|
|
230
239
|
</span>
|
|
231
|
-
<button
|
|
232
|
-
type="button"
|
|
233
|
-
onClick={() => paletteActions.setMode("commands", text)}
|
|
234
|
-
className="h-8 sm:hidden"
|
|
235
|
-
>
|
|
236
|
-
Commands
|
|
237
|
-
</button>
|
|
238
240
|
<span className="ml-auto font-mono">
|
|
239
241
|
{searching && data
|
|
240
242
|
? `${groups.length} ${groups.length === 1 ? "chat" : "chats"} · ${data.tookMs} ms`
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Scale,
|
|
24
24
|
} from "lucide-react";
|
|
25
25
|
import type { Session } from "@/lib/db";
|
|
26
|
+
import type { SessionStatus } from "@/components/views/types";
|
|
26
27
|
import type { PaletteCommand } from "@/lib/palette/registry";
|
|
27
28
|
import { useWorkspacesQuery } from "@/data/workspaces";
|
|
28
29
|
import { useRecipesQuery } from "@/data/recipes";
|
|
@@ -57,13 +58,25 @@ function switchMode(current: string | undefined, to: "light" | "dark"): string {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
const STATE: Record<string, { status: "running" | "waiting"; detail: string }> =
|
|
62
|
+
{
|
|
63
|
+
waiting: { status: "waiting", detail: "Waiting on you" },
|
|
64
|
+
running: { status: "running", detail: "Working" },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function sessionState(s: SessionStatus | undefined) {
|
|
68
|
+
return s?.need || (s?.asks ?? 0) > 0 ? STATE.waiting : STATE[s?.status ?? ""];
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
// The palette's app-wide commands and its session search.
|
|
61
72
|
export function useAppCommands({
|
|
62
73
|
sessions,
|
|
74
|
+
statuses,
|
|
63
75
|
onSelectSession,
|
|
64
76
|
onSearchCode,
|
|
65
77
|
}: {
|
|
66
78
|
sessions: Session[];
|
|
79
|
+
statuses: Record<string, SessionStatus>;
|
|
67
80
|
onSelectSession: (session: Session) => void;
|
|
68
81
|
onSearchCode: () => void;
|
|
69
82
|
}) {
|
|
@@ -260,6 +273,7 @@ export function useAppCommands({
|
|
|
260
273
|
group: "Sessions",
|
|
261
274
|
keywords: [s.working_directory, s.branch_name ?? ""].filter(Boolean),
|
|
262
275
|
icon: MessageSquare,
|
|
276
|
+
...sessionState(statuses[s.id]),
|
|
263
277
|
run: () => onSelectSession(s),
|
|
264
278
|
}))
|
|
265
279
|
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { MessagesSquare, Search } from "lucide-react";
|
|
5
|
+
import type { PaletteCommand } from "@/lib/palette/registry";
|
|
6
|
+
import { MIN_QUERY, useConversationSearch } from "@/data/search";
|
|
7
|
+
import { useDebouncedValue } from "@/hooks/useDebouncedValue";
|
|
8
|
+
import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
9
|
+
import { paletteActions } from "@/stores/palette";
|
|
10
|
+
import { plain } from "./conversations/format";
|
|
11
|
+
import { useOpenHit } from "./conversations/useOpenHit";
|
|
12
|
+
|
|
13
|
+
// Conversation matches offered last in All.
|
|
14
|
+
const CONVERSATIONS = 3;
|
|
15
|
+
|
|
16
|
+
// The few conversations a query finds, then a way into the full search.
|
|
17
|
+
export function useConversationRows(
|
|
18
|
+
query: string,
|
|
19
|
+
enabled: boolean
|
|
20
|
+
): PaletteCommand[] {
|
|
21
|
+
const { workspace } = useSelectedWorkspace();
|
|
22
|
+
const debounced = useDebouncedValue(query.trim(), 150);
|
|
23
|
+
const openHit = useOpenHit();
|
|
24
|
+
const { data: found } = useConversationSearch(
|
|
25
|
+
debounced,
|
|
26
|
+
workspace?.id ?? "all",
|
|
27
|
+
{ limit: CONVERSATIONS, enabled }
|
|
28
|
+
);
|
|
29
|
+
return useMemo(() => {
|
|
30
|
+
const q = query.trim();
|
|
31
|
+
if (!enabled || q.length < MIN_QUERY || q !== debounced) return [];
|
|
32
|
+
const hits = (found?.groups ?? []).flatMap((group) =>
|
|
33
|
+
group.hits.slice(0, 1).map((hit) => ({
|
|
34
|
+
id: `conversation:${group.sessionId}:${hit.itemId}`,
|
|
35
|
+
title: group.name,
|
|
36
|
+
detail: plain(hit.snippet),
|
|
37
|
+
group: "Conversations",
|
|
38
|
+
icon: MessagesSquare,
|
|
39
|
+
run: () => openHit(group, hit),
|
|
40
|
+
keepOpen: true,
|
|
41
|
+
}))
|
|
42
|
+
);
|
|
43
|
+
return [
|
|
44
|
+
...hits,
|
|
45
|
+
{
|
|
46
|
+
id: "conversation:all",
|
|
47
|
+
title: `Search all conversations for “${q}”`,
|
|
48
|
+
group: "Conversations",
|
|
49
|
+
icon: Search,
|
|
50
|
+
keepOpen: true,
|
|
51
|
+
run: () => paletteActions.setTab("conversations", q),
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
}, [found, query, debounced, enabled, openHit]);
|
|
55
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { openSessionLink } from "@/components/Chat/session-link";
|
|
4
4
|
import { compactTimeAgo } from "@/lib/session-meta";
|
|
5
|
-
import {
|
|
5
|
+
import { sessionPath } from "@/lib/session-url";
|
|
6
6
|
import type { EnvSessionView } from "@/lib/health/env/types";
|
|
7
7
|
|
|
8
8
|
export function SessionList({
|
|
@@ -27,7 +27,7 @@ export function SessionList({
|
|
|
27
27
|
{sessions.map((s) => (
|
|
28
28
|
<li key={s.sessionId} className="min-h-11 text-sm">
|
|
29
29
|
<a
|
|
30
|
-
href={
|
|
30
|
+
href={sessionPath({ id: s.sessionId, name: s.name })}
|
|
31
31
|
onClick={(e) => openSessionLink(e, s.sessionId)}
|
|
32
32
|
className="font-medium underline-offset-2 hover:underline"
|
|
33
33
|
>
|
|
@@ -103,7 +103,7 @@ export function PaneMenu({
|
|
|
103
103
|
</div>
|
|
104
104
|
)}
|
|
105
105
|
</DropdownMenuLabel>
|
|
106
|
-
<DropdownMenuItem onSelect={() => copySessionLink(session
|
|
106
|
+
<DropdownMenuItem onSelect={() => copySessionLink(session)}>
|
|
107
107
|
<Link className="h-4 w-4" />
|
|
108
108
|
Copy link
|
|
109
109
|
</DropdownMenuItem>
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
queuedSubtitle,
|
|
13
13
|
type QueuedRow as Row,
|
|
14
14
|
} from "@/lib/sidebar/queued";
|
|
15
|
-
import {
|
|
15
|
+
import { SessionAnchor } from "@/components/Chat/SessionAnchor";
|
|
16
16
|
import { cn } from "@/lib/utils";
|
|
17
17
|
import { useRowContext } from "./RowContext";
|
|
18
18
|
import { RowShell, type RowMenuKind } from "./RowShell";
|
|
@@ -184,8 +184,8 @@ function QueuedSubtitle({ row }: { row: Row }) {
|
|
|
184
184
|
(target ? (
|
|
185
185
|
<>
|
|
186
186
|
{" "}
|
|
187
|
-
<
|
|
188
|
-
|
|
187
|
+
<SessionAnchor
|
|
188
|
+
sessionId={target}
|
|
189
189
|
onClick={(e) => {
|
|
190
190
|
// The row opens it too; the link alone handles its click.
|
|
191
191
|
e.stopPropagation();
|
|
@@ -198,7 +198,7 @@ function QueuedSubtitle({ row }: { row: Row }) {
|
|
|
198
198
|
className="hover:text-foreground underline decoration-dotted underline-offset-2"
|
|
199
199
|
>
|
|
200
200
|
{after}
|
|
201
|
-
</
|
|
201
|
+
</SessionAnchor>
|
|
202
202
|
</>
|
|
203
203
|
) : (
|
|
204
204
|
` ${after}`
|
|
@@ -191,7 +191,7 @@ export function SessionRowMenu({
|
|
|
191
191
|
recipesUiActions.openRecipe(recipeRun!.id)
|
|
192
192
|
);
|
|
193
193
|
case "copy-link":
|
|
194
|
-
return item("Copy link", Link, () => copySessionLink(session
|
|
194
|
+
return item("Copy link", Link, () => copySessionLink(session));
|
|
195
195
|
case "share":
|
|
196
196
|
return item("Share…", Share2, () =>
|
|
197
197
|
shareUiActions.open({
|