@saadnvd1/agent-os 0.3.673 → 0.3.680
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/README.md +2 -1
- package/app/api/bus/read/route.ts +34 -0
- package/app/api/search/conversations/context/route.ts +26 -0
- package/app/api/search/conversations/route.test.ts +139 -0
- package/app/api/search/conversations/route.ts +25 -0
- package/app/globals.css +25 -0
- package/components/Bookmarks/BookmarksDialog.tsx +2 -8
- package/components/Chat/useGoToItem.ts +11 -0
- package/components/CommandPalette/CommandPalette.tsx +23 -156
- package/components/CommandPalette/CommandsBody.tsx +216 -0
- package/components/CommandPalette/conversations/ConversationSearch.tsx +246 -0
- package/components/CommandPalette/conversations/EmptyState.tsx +95 -0
- package/components/CommandPalette/conversations/Preview.tsx +129 -0
- package/components/CommandPalette/conversations/ResultList.tsx +91 -0
- package/components/CommandPalette/conversations/Snippet.tsx +24 -0
- package/components/CommandPalette/conversations/format.ts +30 -0
- package/components/CommandPalette/conversations/recent.test.ts +20 -0
- package/components/CommandPalette/conversations/recent.ts +35 -0
- package/components/CommandPalette/conversations/useOpenHit.ts +30 -0
- package/data/search/index.ts +65 -0
- package/data/search/keys.ts +7 -0
- package/hooks/useDebouncedValue.ts +11 -0
- package/lib/agents/brief.ts +8 -0
- package/lib/bus/condense.ts +242 -0
- package/lib/bus/read-scope.ts +109 -0
- package/lib/bus/read.ts +181 -0
- package/lib/chat/worker/host.ts +3 -1
- package/lib/db/migrations/20261019T1500_conversation_search.ts +12 -0
- package/lib/demo-reseed.ts +4 -0
- package/lib/health/facts.ts +13 -3
- package/lib/health/spin-tick.ts +293 -0
- package/lib/health/spin.ts +158 -0
- package/lib/health/tick.ts +10 -2
- package/lib/health/types.ts +2 -0
- package/lib/orchestrator/read.ts +18 -4
- package/lib/palette/registry.ts +4 -0
- package/lib/resurrect/index.ts +2 -3
- package/lib/search/conversations/backfill.ts +117 -0
- package/lib/search/conversations/buildMatchQuery.ts +31 -0
- package/lib/search/conversations/index.ts +9 -0
- package/lib/search/conversations/messageContext.ts +45 -0
- package/lib/search/conversations/schema.ts +71 -0
- package/lib/search/conversations/searchConversations.ts +151 -0
- package/lib/search/conversations/types.ts +45 -0
- package/lib/sidebar/row-actions.ts +19 -8
- package/lib/sidebar/row-menu.ts +1 -1
- package/lib/tasks/restorable.ts +16 -0
- package/package.json +1 -1
- package/scripts/bench-conversation-search.ts +73 -0
- package/server.ts +3 -0
- package/stores/chatJump.ts +10 -0
- package/stores/palette.ts +22 -4
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { getDb } from "@/lib/db";
|
|
2
|
+
import { buildMatchQuery } from "./buildMatchQuery";
|
|
3
|
+
import { indexProgress } from "./backfill";
|
|
4
|
+
import type {
|
|
5
|
+
ConversationGroup,
|
|
6
|
+
ConversationHit,
|
|
7
|
+
SearchResponse,
|
|
8
|
+
SearchScope,
|
|
9
|
+
SnippetSegment,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
// Best matches pulled before they are grouped by conversation.
|
|
13
|
+
const CANDIDATES = 200;
|
|
14
|
+
const HITS_PER_SESSION = 3;
|
|
15
|
+
export const MAX_GROUPS = 30;
|
|
16
|
+
|
|
17
|
+
const OPEN = "\u0002";
|
|
18
|
+
const CLOSE = "\u0003";
|
|
19
|
+
|
|
20
|
+
interface Row {
|
|
21
|
+
session_id: string;
|
|
22
|
+
item_id: string;
|
|
23
|
+
snippet: string;
|
|
24
|
+
kind: "user" | "assistant";
|
|
25
|
+
created_at: number | null;
|
|
26
|
+
name: string;
|
|
27
|
+
status: string;
|
|
28
|
+
task_status: string | null;
|
|
29
|
+
archived_at: string | null;
|
|
30
|
+
updated_at: string | null;
|
|
31
|
+
project_id: string | null;
|
|
32
|
+
project_name: string | null;
|
|
33
|
+
workspace_id: string | null;
|
|
34
|
+
workspace_name: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseSnippet(raw: string): SnippetSegment[] {
|
|
38
|
+
const out: SnippetSegment[] = [];
|
|
39
|
+
let hit = false;
|
|
40
|
+
let text = "";
|
|
41
|
+
for (const ch of raw) {
|
|
42
|
+
if (ch === OPEN || ch === CLOSE) {
|
|
43
|
+
if (text) out.push({ text, hit });
|
|
44
|
+
text = "";
|
|
45
|
+
hit = ch === OPEN;
|
|
46
|
+
} else text += ch;
|
|
47
|
+
}
|
|
48
|
+
if (text) out.push({ text, hit });
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// done/dropped/merged tasks and archived sessions keep their conversation,
|
|
53
|
+
// so the label says what became of the session.
|
|
54
|
+
function statusOf(r: Row): string {
|
|
55
|
+
if (r.archived_at) return "archived";
|
|
56
|
+
if (r.task_status && r.task_status !== "running") return r.task_status;
|
|
57
|
+
return r.status;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function searchConversations(
|
|
61
|
+
q: string,
|
|
62
|
+
scope: SearchScope,
|
|
63
|
+
page: { offset?: number; limit?: number } = {}
|
|
64
|
+
): SearchResponse {
|
|
65
|
+
const started = performance.now();
|
|
66
|
+
const parsed = buildMatchQuery(q);
|
|
67
|
+
const empty: SearchResponse = {
|
|
68
|
+
groups: [],
|
|
69
|
+
more: false,
|
|
70
|
+
tookMs: 0,
|
|
71
|
+
indexing: indexProgress(),
|
|
72
|
+
terms: [],
|
|
73
|
+
};
|
|
74
|
+
if (!parsed) return empty;
|
|
75
|
+
|
|
76
|
+
const where: string[] = [];
|
|
77
|
+
const args: unknown[] = [parsed.match];
|
|
78
|
+
if (scope.workspace !== "all") {
|
|
79
|
+
where.push(`COALESCE(s.workspace_id, p.workspace_id) = ?`);
|
|
80
|
+
args.push(scope.workspace);
|
|
81
|
+
}
|
|
82
|
+
if (scope.project) {
|
|
83
|
+
where.push(`s.project_id = ?`);
|
|
84
|
+
args.push(scope.project);
|
|
85
|
+
}
|
|
86
|
+
args.push(CANDIDATES);
|
|
87
|
+
const rows = getDb()
|
|
88
|
+
.prepare(
|
|
89
|
+
`WITH hits AS (
|
|
90
|
+
SELECT k.session_id, k.item_id, bm25(chat_search) AS rank,
|
|
91
|
+
snippet(chat_search, 0, '${OPEN}', '${CLOSE}', '…', 18) AS snippet
|
|
92
|
+
FROM chat_search
|
|
93
|
+
JOIN chat_search_keys k ON k.id = chat_search.rowid
|
|
94
|
+
JOIN sessions s ON s.id = k.session_id
|
|
95
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
96
|
+
WHERE chat_search MATCH ? ${where.map((w) => `AND ${w}`).join(" ")}
|
|
97
|
+
ORDER BY rank LIMIT ?
|
|
98
|
+
)
|
|
99
|
+
SELECT h.session_id, h.item_id, h.snippet, c.kind,
|
|
100
|
+
json_extract(c.data, '$.createdAt') AS created_at,
|
|
101
|
+
s.name, s.status, s.task_status, s.archived_at, s.updated_at,
|
|
102
|
+
s.project_id, p.name AS project_name,
|
|
103
|
+
COALESCE(s.workspace_id, p.workspace_id) AS workspace_id,
|
|
104
|
+
w.name AS workspace_name
|
|
105
|
+
FROM hits h
|
|
106
|
+
JOIN chat_items c ON c.session_id = h.session_id AND c.item_id = h.item_id
|
|
107
|
+
JOIN session_rows s ON s.id = h.session_id
|
|
108
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
109
|
+
LEFT JOIN workspaces w ON w.id = COALESCE(s.workspace_id, p.workspace_id)
|
|
110
|
+
ORDER BY h.rank`
|
|
111
|
+
)
|
|
112
|
+
.all(...args) as Row[];
|
|
113
|
+
|
|
114
|
+
const groups = new Map<string, ConversationGroup>();
|
|
115
|
+
for (const r of rows) {
|
|
116
|
+
let g = groups.get(r.session_id);
|
|
117
|
+
if (!g) {
|
|
118
|
+
g = {
|
|
119
|
+
sessionId: r.session_id,
|
|
120
|
+
name: r.name,
|
|
121
|
+
projectId: r.project_id,
|
|
122
|
+
projectName: r.project_name,
|
|
123
|
+
workspaceId: r.workspace_id,
|
|
124
|
+
workspaceName: r.workspace_name,
|
|
125
|
+
status: statusOf(r),
|
|
126
|
+
updatedAt: r.updated_at,
|
|
127
|
+
hits: [],
|
|
128
|
+
};
|
|
129
|
+
groups.set(r.session_id, g);
|
|
130
|
+
}
|
|
131
|
+
if (g.hits.length < HITS_PER_SESSION) {
|
|
132
|
+
const hit: ConversationHit = {
|
|
133
|
+
itemId: r.item_id,
|
|
134
|
+
role: r.kind,
|
|
135
|
+
createdAt: r.created_at,
|
|
136
|
+
snippet: parseSnippet(r.snippet),
|
|
137
|
+
};
|
|
138
|
+
g.hits.push(hit);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const all = [...groups.values()];
|
|
142
|
+
const offset = Math.max(0, page.offset ?? 0);
|
|
143
|
+
const limit = Math.min(MAX_GROUPS, Math.max(1, page.limit ?? 20));
|
|
144
|
+
return {
|
|
145
|
+
groups: all.slice(offset, offset + limit),
|
|
146
|
+
more: all.length > offset + limit,
|
|
147
|
+
tookMs: Math.round((performance.now() - started) * 10) / 10,
|
|
148
|
+
indexing: indexProgress(),
|
|
149
|
+
terms: parsed.terms,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type SnippetSegment = { text: string; hit: boolean };
|
|
2
|
+
|
|
3
|
+
export interface ConversationHit {
|
|
4
|
+
itemId: string;
|
|
5
|
+
role: "user" | "assistant";
|
|
6
|
+
createdAt: number | null;
|
|
7
|
+
snippet: SnippetSegment[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ConversationGroup {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
projectId: string | null;
|
|
14
|
+
projectName: string | null;
|
|
15
|
+
workspaceId: string | null;
|
|
16
|
+
workspaceName: string | null;
|
|
17
|
+
status: string;
|
|
18
|
+
updatedAt: string | null;
|
|
19
|
+
hits: ConversationHit[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SearchScope {
|
|
23
|
+
// A workspace id, or "all".
|
|
24
|
+
workspace: string;
|
|
25
|
+
project?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SearchResponse {
|
|
29
|
+
groups: ConversationGroup[];
|
|
30
|
+
// More sessions exist past this page.
|
|
31
|
+
more: boolean;
|
|
32
|
+
tookMs: number;
|
|
33
|
+
// The first backfill is still running, so older conversations may be missing.
|
|
34
|
+
indexing: number | null;
|
|
35
|
+
// The words to mark in a preview.
|
|
36
|
+
terms: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ContextItem {
|
|
40
|
+
id: string;
|
|
41
|
+
role: "user" | "assistant";
|
|
42
|
+
text: string;
|
|
43
|
+
createdAt: number | null;
|
|
44
|
+
hit: boolean;
|
|
45
|
+
}
|
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import type { Session } from "@/lib/db/types";
|
|
2
|
+
import { isOverTask, isRestorableTask } from "@/lib/tasks/restorable";
|
|
3
|
+
import type { TaskState } from "@/lib/tasks/state";
|
|
2
4
|
|
|
3
5
|
// What a row's ⋯ menu offers to finish it: Done (merge through the gates,
|
|
4
|
-
// stop, leave it on the Done shelf) for a task, Archive (put it away)
|
|
5
|
-
// every session but an orchestrator. A plain session is finished with
|
|
6
|
-
// done, so it only gets Archive here. A task that
|
|
7
|
-
//
|
|
6
|
+
// stop, leave it on the Done shelf) for a live task, Archive (put it away)
|
|
7
|
+
// for every session but an orchestrator. A plain session is finished with
|
|
8
|
+
// Mark done, so it only gets Archive here. A task that has ended is never
|
|
9
|
+
// finished again: a dropped or done one is restored (exactly what
|
|
10
|
+
// resurrectSession accepts), a merged or moved one only archived.
|
|
8
11
|
export type FinishAction = "done" | "restore" | "archive";
|
|
9
12
|
|
|
10
13
|
export function finishActions(
|
|
11
14
|
session: Pick<Session, "role" | "task_status">,
|
|
12
|
-
plain: boolean
|
|
15
|
+
plain: boolean,
|
|
16
|
+
derived?: TaskState
|
|
13
17
|
): FinishAction[] {
|
|
14
18
|
if (session.role === "orchestrator") return [];
|
|
15
19
|
if (plain) return ["archive"];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
if (isRestorableTask(session.task_status)) return ["restore", "archive"];
|
|
21
|
+
// The PR can merge before the status catches up.
|
|
22
|
+
if (
|
|
23
|
+
isOverTask(session.task_status) ||
|
|
24
|
+
derived === "merged" ||
|
|
25
|
+
derived === "dropped" ||
|
|
26
|
+
derived === "done"
|
|
27
|
+
)
|
|
28
|
+
return ["archive"];
|
|
29
|
+
return ["done", "archive"];
|
|
19
30
|
}
|
package/lib/sidebar/row-menu.ts
CHANGED
|
@@ -94,7 +94,7 @@ function group(
|
|
|
94
94
|
export function rowMenu(input: RowMenuInput): RowMenuPlan {
|
|
95
95
|
const { session: s, row, remote } = input;
|
|
96
96
|
const plain = !!row && !row.task && !s.task_status;
|
|
97
|
-
const finish = finishActions(s, plain);
|
|
97
|
+
const finish = finishActions(s, plain, row?.task);
|
|
98
98
|
const sections: RowMenuEntry[][] = [];
|
|
99
99
|
|
|
100
100
|
// A quick change has no agent: its PR and the ways to finish it.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Which task statuses are over, and which of those resurrect will bring
|
|
2
|
+
// back. One rule for the Restore button and for resurrectSession.
|
|
3
|
+
// Client-safe: no database.
|
|
4
|
+
|
|
5
|
+
// A dropped or done task keeps its branch and conversation, so it restores.
|
|
6
|
+
export const RESTORABLE_TASK = new Set(["dropped", "done"]);
|
|
7
|
+
|
|
8
|
+
// A merged task's work is in the base branch; a moved one runs elsewhere.
|
|
9
|
+
// Neither restores, and neither is finished again.
|
|
10
|
+
const OVER_TASK = new Set([...RESTORABLE_TASK, "merged", "moved"]);
|
|
11
|
+
|
|
12
|
+
export const isRestorableTask = (status: string | null | undefined) =>
|
|
13
|
+
!!status && RESTORABLE_TASK.has(status);
|
|
14
|
+
|
|
15
|
+
export const isOverTask = (status: string | null | undefined) =>
|
|
16
|
+
!!status && OVER_TASK.has(status);
|
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Times conversation search on a copy of a real database.
|
|
2
|
+
// npx tsx scripts/bench-conversation-search.ts <path to agent-os.db>
|
|
3
|
+
// The source is only read (SQLite's online backup); everything else happens on
|
|
4
|
+
// the copy, which is removed afterwards.
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const source = process.argv[2];
|
|
11
|
+
if (!source) {
|
|
12
|
+
console.error("usage: bench-conversation-search.ts <agent-os.db>");
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
const dir = mkdtempSync(join(tmpdir(), "aos-bench-"));
|
|
16
|
+
const copy = join(dir, "bench.db");
|
|
17
|
+
|
|
18
|
+
const QUERIES = [
|
|
19
|
+
"co",
|
|
20
|
+
"the",
|
|
21
|
+
"compac",
|
|
22
|
+
"worktree",
|
|
23
|
+
'"merge gate"',
|
|
24
|
+
"sqlite fts",
|
|
25
|
+
"orchestrator brake",
|
|
26
|
+
"pull request",
|
|
27
|
+
"a-b: c++",
|
|
28
|
+
"xyzzyplugh",
|
|
29
|
+
];
|
|
30
|
+
const RUNS = 20;
|
|
31
|
+
|
|
32
|
+
async function main() {
|
|
33
|
+
const src = new Database(source, { readonly: true });
|
|
34
|
+
await src.backup(copy);
|
|
35
|
+
src.close();
|
|
36
|
+
process.env.DB_PATH = copy;
|
|
37
|
+
const { getDb } = await import("../lib/db");
|
|
38
|
+
const { backfillStep } = await import("../lib/search/conversations/backfill");
|
|
39
|
+
const { searchConversations } =
|
|
40
|
+
await import("../lib/search/conversations/searchConversations");
|
|
41
|
+
const db = getDb();
|
|
42
|
+
const t0 = performance.now();
|
|
43
|
+
while (!backfillStep(db));
|
|
44
|
+
const backfillMs = Math.round(performance.now() - t0);
|
|
45
|
+
const rows = db
|
|
46
|
+
.prepare(`SELECT COUNT(*) AS n FROM chat_search_keys`)
|
|
47
|
+
.get() as { n: number };
|
|
48
|
+
const size = db
|
|
49
|
+
.prepare(
|
|
50
|
+
`SELECT SUM(pgsize) AS b FROM dbstat WHERE name LIKE 'chat_search%'`
|
|
51
|
+
)
|
|
52
|
+
.get() as { b: number };
|
|
53
|
+
console.log(
|
|
54
|
+
`indexed ${rows.n} messages in ${backfillMs} ms; index ${(size.b / 1048576).toFixed(1)} MB`
|
|
55
|
+
);
|
|
56
|
+
for (const q of QUERIES) {
|
|
57
|
+
const ms: number[] = [];
|
|
58
|
+
let groups = 0;
|
|
59
|
+
for (let i = 0; i < RUNS; i++) {
|
|
60
|
+
const t = performance.now();
|
|
61
|
+
groups = searchConversations(q, { workspace: "all" }).groups.length;
|
|
62
|
+
ms.push(performance.now() - t);
|
|
63
|
+
}
|
|
64
|
+
ms.sort((a, b) => a - b);
|
|
65
|
+
const at = (p: number) =>
|
|
66
|
+
ms[Math.min(ms.length - 1, Math.floor(p * ms.length))];
|
|
67
|
+
console.log(
|
|
68
|
+
`${q.padEnd(22)} p50 ${at(0.5).toFixed(1).padStart(5)} ms p95 ${at(0.95).toFixed(1).padStart(5)} ms ${groups} sessions`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main().finally(() => rmSync(dir, { recursive: true, force: true }));
|
package/server.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { reattachChats } from "./lib/chat/runner";
|
|
|
9
9
|
import { serveChatSocket } from "./lib/chat/socket";
|
|
10
10
|
import { startStackWatcher } from "./lib/stacks";
|
|
11
11
|
import { startPauseWatcher } from "./lib/tasks/pause";
|
|
12
|
+
import { startConversationBackfill } from "./lib/search/conversations/backfill";
|
|
12
13
|
import { setStartGate } from "./lib/stacks/tick";
|
|
13
14
|
import { stackStartGate } from "./lib/orchestrator/brakes";
|
|
14
15
|
import { buildId } from "./lib/build";
|
|
@@ -433,6 +434,8 @@ app.prepare().then(async () => {
|
|
|
433
434
|
setInterval(sweepSims, 5 * 60_000);
|
|
434
435
|
// Chat turns that kept running through a restart.
|
|
435
436
|
void reattachChats().finally(() => startPauseWatcher());
|
|
437
|
+
// Older chats get into the search index in the background, a batch at a time.
|
|
438
|
+
if (process.env.AGENTOS_SEARCH_INDEX !== "off") startConversationBackfill();
|
|
436
439
|
// Reviews this restart cut off run again and deliver their verdicts.
|
|
437
440
|
if (process.env.AGENTOS_ORCHESTRATOR !== "off")
|
|
438
441
|
resumeReviews().catch((error) =>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { hrefWithSession } from "@/lib/session-url";
|
|
2
|
+
import { bookmarksUiActions } from "@/stores/bookmarksUi";
|
|
3
|
+
|
|
4
|
+
// Opens a chat by its address, then takes it to one message (bookmarks and
|
|
5
|
+
// search both end here). The chat takes the jump once it is on screen.
|
|
6
|
+
export function openChatAt(sessionId: string, itemId: string): void {
|
|
7
|
+
window.history.pushState(null, "", hrefWithSession(location.href, sessionId));
|
|
8
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
9
|
+
bookmarksUiActions.jump(sessionId, itemId);
|
|
10
|
+
}
|
package/stores/palette.ts
CHANGED
|
@@ -8,20 +8,38 @@ export interface PalettePicker {
|
|
|
8
8
|
items: PaletteCommand[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
export type PaletteMode = "commands" | "conversations";
|
|
12
|
+
|
|
13
|
+
export const paletteUi = proxy<{
|
|
14
|
+
open: boolean;
|
|
15
|
+
picker: PalettePicker | null;
|
|
16
|
+
mode: PaletteMode;
|
|
17
|
+
// What was typed when the mode changed, so the other mode starts with it.
|
|
18
|
+
query: string;
|
|
19
|
+
}>({ open: false, picker: null, mode: "commands", query: "" });
|
|
14
20
|
|
|
15
21
|
export const paletteActions = {
|
|
16
22
|
open: () => {
|
|
17
23
|
paletteUi.picker = null;
|
|
24
|
+
paletteUi.mode = "commands";
|
|
25
|
+
paletteUi.query = "";
|
|
18
26
|
paletteUi.open = true;
|
|
19
27
|
},
|
|
20
28
|
setOpen: (open: boolean) => {
|
|
21
29
|
paletteUi.open = open;
|
|
22
|
-
if (!open)
|
|
30
|
+
if (!open) {
|
|
31
|
+
paletteUi.picker = null;
|
|
32
|
+
paletteUi.mode = "commands";
|
|
33
|
+
paletteUi.query = "";
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
setMode: (mode: PaletteMode, query = "") => {
|
|
37
|
+
paletteUi.picker = null;
|
|
38
|
+
paletteUi.mode = mode;
|
|
39
|
+
paletteUi.query = query;
|
|
23
40
|
},
|
|
24
41
|
pick: (placeholder: string, items: PaletteCommand[]) => {
|
|
42
|
+
paletteUi.mode = "commands";
|
|
25
43
|
paletteUi.picker = ref({
|
|
26
44
|
placeholder,
|
|
27
45
|
items: items.map((c, i) => (i < 9 ? { ...c, hint: `⌘${i + 1}` } : c)),
|