offsite 0.0.55 → 0.0.57
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 +8 -0
- package/apps/client/dist/assets/TextWrapIcon-BRhMbXe9.js +1 -0
- package/apps/client/dist/assets/{TextWrapIcon-CaHR7d2a.js → XIcon-BF9rDynM.js} +1 -1
- package/apps/client/dist/assets/{context-menu-CniNpkSW.js → context-menu-ah5Njpdz.js} +9 -9
- package/apps/client/dist/assets/{diff-panel-DfyRZtVh.js → diff-panel-BKiJiAiZ.js} +5 -5
- package/apps/client/dist/assets/{dist-D9LSGRim.js → dist-Cgj97HqD.js} +1 -1
- package/apps/client/dist/assets/{editor-dialog-Bc1Gpfll.js → editor-dialog-DY6qlmWC.js} +2 -2
- package/apps/client/dist/assets/{image-viewer-BPClqXmZ.js → image-viewer-DVmKXAyv.js} +1 -1
- package/apps/client/dist/assets/index-BtnFX6Z2.css +2 -0
- package/apps/client/dist/assets/index-CfER-bxq.js +21 -0
- package/apps/client/dist/assets/{preview-dialog-DvyH_XYP.js → preview-dialog-B-LOxdLg.js} +67 -67
- package/apps/client/dist/assets/{video-viewer-BATGlwTk.js → video-viewer-BBAwsDzR.js} +1 -1
- package/apps/client/dist/index.html +6 -6
- package/apps/client/dist/sw.js +1 -1
- package/apps/server/dist/main.mjs +353 -25
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/server/drizzle/20261031083530_session_opens/migration.sql +6 -0
- package/apps/server/drizzle/20261031083530_session_opens/snapshot.json +378 -0
- package/package.json +1 -1
- package/apps/client/dist/assets/XIcon-C8QiXos4.js +0 -1
- package/apps/client/dist/assets/index-BwVrIQyk.js +0 -21
- package/apps/client/dist/assets/index-DkhFb5Mr.css +0 -2
|
@@ -21,7 +21,7 @@ import { sumBy } from "es-toolkit";
|
|
|
21
21
|
import { DatabaseSync } from "node:sqlite";
|
|
22
22
|
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
23
23
|
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
24
|
-
import { integer, snakeCase, text } from "drizzle-orm/sqlite-core";
|
|
24
|
+
import { integer, real, snakeCase, text } from "drizzle-orm/sqlite-core";
|
|
25
25
|
import sharp from "sharp";
|
|
26
26
|
import { promisify } from "node:util";
|
|
27
27
|
import webPush from "web-push";
|
|
@@ -1597,6 +1597,30 @@ async function codexThread(threadId) {
|
|
|
1597
1597
|
includeTurns: false
|
|
1598
1598
|
})).thread;
|
|
1599
1599
|
}
|
|
1600
|
+
async function codexSearch(term) {
|
|
1601
|
+
return (await appServer(codexBaseDir).request("thread/search", {
|
|
1602
|
+
searchTerm: term,
|
|
1603
|
+
limit: 50,
|
|
1604
|
+
sortKey: "updated_at",
|
|
1605
|
+
sourceKinds: [
|
|
1606
|
+
"cli",
|
|
1607
|
+
"vscode",
|
|
1608
|
+
"appServer"
|
|
1609
|
+
]
|
|
1610
|
+
})).data;
|
|
1611
|
+
}
|
|
1612
|
+
async function codexMessages(threadId) {
|
|
1613
|
+
return (await readThread(threadId)).turns.flatMap((turn) => turn.items.flatMap((item) => {
|
|
1614
|
+
if (item.type === "userMessage") return [{
|
|
1615
|
+
author: "user",
|
|
1616
|
+
text: item.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n")
|
|
1617
|
+
}];
|
|
1618
|
+
return item.type === "agentMessage" ? [{
|
|
1619
|
+
author: "assistant",
|
|
1620
|
+
text: item.text
|
|
1621
|
+
}] : [];
|
|
1622
|
+
}));
|
|
1623
|
+
}
|
|
1600
1624
|
async function codexHistory(sessionId) {
|
|
1601
1625
|
const run = codexRun(sessionId);
|
|
1602
1626
|
const items = turnItems((await readThread(sessionId)).turns, null);
|
|
@@ -1694,6 +1718,118 @@ async function readThread(threadId) {
|
|
|
1694
1718
|
includeTurns: true
|
|
1695
1719
|
})).thread;
|
|
1696
1720
|
}
|
|
1721
|
+
const WORD_START = .8;
|
|
1722
|
+
const MID_WORD = .3;
|
|
1723
|
+
const PHRASE_BONUS = .25;
|
|
1724
|
+
const MID_WORD_LENGTH = 4;
|
|
1725
|
+
const SNIPPET_LEAD = 24;
|
|
1726
|
+
const SNIPPET_LENGTH = 160;
|
|
1727
|
+
const alphanumeric = /[\p{L}\p{N}]/u;
|
|
1728
|
+
const lowercase = /\p{Ll}/u;
|
|
1729
|
+
const uppercase = /\p{Lu}/u;
|
|
1730
|
+
const digit = /\p{N}/u;
|
|
1731
|
+
function fold(text) {
|
|
1732
|
+
return text.replace(/[\u0080-]/g, (character) => {
|
|
1733
|
+
const folded = character.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
|
|
1734
|
+
return folded.length === 1 ? folded : character;
|
|
1735
|
+
}).toLowerCase();
|
|
1736
|
+
}
|
|
1737
|
+
function searchWords(query) {
|
|
1738
|
+
return fold(query).replace(/^["'`]+|["'`]+$/g, "").split(/\s+/).filter(Boolean);
|
|
1739
|
+
}
|
|
1740
|
+
function hasSearchableWord(query) {
|
|
1741
|
+
return searchWords(query).some((word) => word.length >= 3);
|
|
1742
|
+
}
|
|
1743
|
+
function isWordStart(text, index) {
|
|
1744
|
+
const character = text[index];
|
|
1745
|
+
const previous = text[index - 1] ?? "";
|
|
1746
|
+
const next = text[index + 1] ?? "";
|
|
1747
|
+
return alphanumeric.test(character) && (previous === "" || !alphanumeric.test(previous) || lowercase.test(previous) && uppercase.test(character) || uppercase.test(previous) && uppercase.test(character) && lowercase.test(next) || digit.test(previous) !== digit.test(character));
|
|
1748
|
+
}
|
|
1749
|
+
function isWordEnd(text, end) {
|
|
1750
|
+
return end >= text.length || !alphanumeric.test(text[end]) || isWordStart(text, end);
|
|
1751
|
+
}
|
|
1752
|
+
function findWord(text, folded, word) {
|
|
1753
|
+
let quality = 0;
|
|
1754
|
+
let at = -1;
|
|
1755
|
+
for (let index = folded.indexOf(word); index !== -1; index = folded.indexOf(word, index + 1)) {
|
|
1756
|
+
const found = !isWordStart(text, index) ? MID_WORD : isWordEnd(text, index + word.length) ? 1 : WORD_START;
|
|
1757
|
+
if (found > quality) {
|
|
1758
|
+
quality = found;
|
|
1759
|
+
at = index;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return at === -1 ? null : {
|
|
1763
|
+
quality,
|
|
1764
|
+
at
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
function matchMessage(query, text) {
|
|
1768
|
+
const words = searchWords(query);
|
|
1769
|
+
if (words.length === 0) return null;
|
|
1770
|
+
const folded = fold(text);
|
|
1771
|
+
let quality = 1;
|
|
1772
|
+
let offset = text.length;
|
|
1773
|
+
for (const word of words) {
|
|
1774
|
+
const found = findWord(text, folded, word);
|
|
1775
|
+
if (found === null || found.quality === MID_WORD && word.length < MID_WORD_LENGTH) return null;
|
|
1776
|
+
if (word.length >= 3) quality = Math.min(quality, found.quality);
|
|
1777
|
+
offset = Math.min(offset, found.at);
|
|
1778
|
+
}
|
|
1779
|
+
const phrase = words.length > 1 ? folded.indexOf(words.join(" ")) : -1;
|
|
1780
|
+
return phrase === -1 ? {
|
|
1781
|
+
quality,
|
|
1782
|
+
offset
|
|
1783
|
+
} : {
|
|
1784
|
+
quality: quality + PHRASE_BONUS,
|
|
1785
|
+
offset: phrase
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function messageSnippet(text, offset) {
|
|
1789
|
+
let start = Math.max(0, offset - SNIPPET_LEAD);
|
|
1790
|
+
const space = text.slice(start, offset).search(/\s/);
|
|
1791
|
+
if (start > 0 && space !== -1) start += space + 1;
|
|
1792
|
+
const shown = text.slice(start, start + 320).replace(/\s+/g, " ").trim();
|
|
1793
|
+
const cut = shown.length > SNIPPET_LENGTH;
|
|
1794
|
+
return `${start > 0 ? "…" : ""}${shown.slice(0, SNIPPET_LENGTH)}${cut ? "…" : ""}`;
|
|
1795
|
+
}
|
|
1796
|
+
// ../core/src/search.ts
|
|
1797
|
+
const OPEN_REPEAT = 6e5;
|
|
1798
|
+
const PICK_WINDOW = 18144e5;
|
|
1799
|
+
const PICK_LIMIT = 3;
|
|
1800
|
+
const PICK_LENGTH = 32;
|
|
1801
|
+
const USAGE_HALF_LIFE = 12096e5;
|
|
1802
|
+
function decay(age) {
|
|
1803
|
+
return 2 ** (-Math.max(0, age) / USAGE_HALF_LIFE);
|
|
1804
|
+
}
|
|
1805
|
+
function storedQuery(query) {
|
|
1806
|
+
return searchWords(query).join(" ").slice(0, PICK_LENGTH).trim();
|
|
1807
|
+
}
|
|
1808
|
+
function nextOpens(previous, at, query) {
|
|
1809
|
+
const pick = storedQuery(query ?? "");
|
|
1810
|
+
const earlier = (previous?.picks ?? []).filter((entry) => at - entry.at <= PICK_WINDOW && entry.query !== pick);
|
|
1811
|
+
const kept = (pick === "" ? earlier : [{
|
|
1812
|
+
query: pick,
|
|
1813
|
+
at
|
|
1814
|
+
}, ...earlier]).slice(0, PICK_LIMIT);
|
|
1815
|
+
if (previous === null) return {
|
|
1816
|
+
score: 1,
|
|
1817
|
+
openedAt: at,
|
|
1818
|
+
picks: kept
|
|
1819
|
+
};
|
|
1820
|
+
if (at - previous.openedAt < OPEN_REPEAT) return {
|
|
1821
|
+
...previous,
|
|
1822
|
+
picks: kept
|
|
1823
|
+
};
|
|
1824
|
+
return {
|
|
1825
|
+
score: previous.score * decay(at - previous.openedAt) + 1,
|
|
1826
|
+
openedAt: at,
|
|
1827
|
+
picks: kept
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
function compareMessages(left, right) {
|
|
1831
|
+
return right.quality - left.quality || Number(right.author === "user") - Number(left.author === "user");
|
|
1832
|
+
}
|
|
1697
1833
|
// src/lib/opencode/items.ts
|
|
1698
1834
|
const toolStatuses = {
|
|
1699
1835
|
streaming: "running",
|
|
@@ -2100,6 +2236,19 @@ async function openCodeHistory(sessionId, cwd) {
|
|
|
2100
2236
|
partial: run?.partial ?? ""
|
|
2101
2237
|
};
|
|
2102
2238
|
}
|
|
2239
|
+
async function openCodeMessages(sessionId) {
|
|
2240
|
+
return (await allMessages(await openCode(), sessionId)).flatMap((message) => {
|
|
2241
|
+
if (message.type === "user") return [{
|
|
2242
|
+
author: "user",
|
|
2243
|
+
text: message.text
|
|
2244
|
+
}];
|
|
2245
|
+
if (message.type !== "assistant") return [];
|
|
2246
|
+
return [{
|
|
2247
|
+
author: "assistant",
|
|
2248
|
+
text: message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n")
|
|
2249
|
+
}];
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2103
2252
|
async function openCodeAgent(agentId, parent) {
|
|
2104
2253
|
return (await allMessages(await openCode(), agentId)).flatMap((message) => messageItems(message, parent));
|
|
2105
2254
|
}
|
|
@@ -2586,6 +2735,12 @@ const sessions$1 = snakeCase.table("sessions", {
|
|
|
2586
2735
|
"minimized"
|
|
2587
2736
|
] }).notNull()
|
|
2588
2737
|
});
|
|
2738
|
+
const sessionOpens = snakeCase.table("session_opens", {
|
|
2739
|
+
sessionId: text().primaryKey(),
|
|
2740
|
+
score: real().notNull(),
|
|
2741
|
+
openedAt: integer().notNull(),
|
|
2742
|
+
picks: text({ mode: "json" }).$type().notNull()
|
|
2743
|
+
});
|
|
2589
2744
|
const pairingCodes = snakeCase.table("pairing_codes", {
|
|
2590
2745
|
hash: text().primaryKey(),
|
|
2591
2746
|
name: text(),
|
|
@@ -2610,7 +2765,7 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
|
2610
2765
|
// src/services/files.ts
|
|
2611
2766
|
const FILE_LIMIT = 2e4;
|
|
2612
2767
|
const OUTPUT_LIMIT$1 = 262144;
|
|
2613
|
-
const READ_LIMIT = 1048576;
|
|
2768
|
+
const READ_LIMIT$1 = 1048576;
|
|
2614
2769
|
const MEDIA_LINK_LIFETIME = 36e5;
|
|
2615
2770
|
const browseToken = randomBytes(32).toString("base64url");
|
|
2616
2771
|
const mediaLinks = /* @__PURE__ */ new Map();
|
|
@@ -2661,10 +2816,10 @@ function readProjectFile(cwd, file) {
|
|
|
2661
2816
|
const full = resolveProjectFile(cwd, file);
|
|
2662
2817
|
const stats = statSync(full);
|
|
2663
2818
|
if (!stats.isFile()) throw new Error(`${file} is not a file`);
|
|
2664
|
-
if (stats.size > READ_LIMIT) return {
|
|
2819
|
+
if (stats.size > READ_LIMIT$1) return {
|
|
2665
2820
|
kind: "large",
|
|
2666
2821
|
size: stats.size,
|
|
2667
|
-
limit: READ_LIMIT
|
|
2822
|
+
limit: READ_LIMIT$1
|
|
2668
2823
|
};
|
|
2669
2824
|
const data = readFileSync(full);
|
|
2670
2825
|
const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
|
|
@@ -2850,7 +3005,7 @@ const UNTRACKED_LIMIT = 100;
|
|
|
2850
3005
|
const NEW_FILE_LIMIT = 262144;
|
|
2851
3006
|
const REPO_DEPTH = 3;
|
|
2852
3007
|
const FETCH_TIMEOUT = 6e4;
|
|
2853
|
-
const run$
|
|
3008
|
+
const run$3 = promisify(execFile);
|
|
2854
3009
|
const PULL_REQUEST_INSTRUCTIONS = `You write GitHub pull requests as Markdown.
|
|
2855
3010
|
|
|
2856
3011
|
## TITLE RULES
|
|
@@ -2937,7 +3092,7 @@ async function sync(cwd) {
|
|
|
2937
3092
|
async function fetchProject(cwd) {
|
|
2938
3093
|
await Promise.all((await projectRepos(cwd)).map(async (repo) => {
|
|
2939
3094
|
try {
|
|
2940
|
-
await run$
|
|
3095
|
+
await run$3("git", ["fetch"], {
|
|
2941
3096
|
cwd: path.join(cwd, repo),
|
|
2942
3097
|
env: {
|
|
2943
3098
|
...process.env,
|
|
@@ -2968,7 +3123,7 @@ async function switchBranch(cwd, branch, create) {
|
|
|
2968
3123
|
async function pullRequestBranches(cwd) {
|
|
2969
3124
|
const head = (await gitStatus(cwd)).branch;
|
|
2970
3125
|
if (head === null) throw new Error("Not on a branch");
|
|
2971
|
-
const base = (await run$
|
|
3126
|
+
const base = (await run$3("gh", [
|
|
2972
3127
|
"repo",
|
|
2973
3128
|
"view",
|
|
2974
3129
|
"--json",
|
|
@@ -2993,7 +3148,7 @@ async function preparePullRequest(cwd, base, commitChanges) {
|
|
|
2993
3148
|
if (commitChanges) await commit(cwd, "", true);
|
|
2994
3149
|
const status = await gitStatus(cwd);
|
|
2995
3150
|
if (status.upstream === null || status.ahead > 0) await push$1(cwd);
|
|
2996
|
-
const open = (await run$
|
|
3151
|
+
const open = (await run$3("gh", [
|
|
2997
3152
|
"pr",
|
|
2998
3153
|
"list",
|
|
2999
3154
|
"--head",
|
|
@@ -3040,7 +3195,7 @@ async function preparePullRequest(cwd, base, commitChanges) {
|
|
|
3040
3195
|
};
|
|
3041
3196
|
}
|
|
3042
3197
|
async function createPullRequest(cwd, request) {
|
|
3043
|
-
const url = (await run$
|
|
3198
|
+
const url = (await run$3("gh", [
|
|
3044
3199
|
"pr",
|
|
3045
3200
|
"create",
|
|
3046
3201
|
"--title",
|
|
@@ -3073,7 +3228,7 @@ async function projectDiff(cwd) {
|
|
|
3073
3228
|
return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), repo === "" ? "" : `${repo}/`)))).join("");
|
|
3074
3229
|
}
|
|
3075
3230
|
function git(cwd, args) {
|
|
3076
|
-
return run$
|
|
3231
|
+
return run$3("git", args, {
|
|
3077
3232
|
cwd,
|
|
3078
3233
|
maxBuffer: 67108864
|
|
3079
3234
|
});
|
|
@@ -3367,6 +3522,13 @@ function expandHome(input) {
|
|
|
3367
3522
|
return input;
|
|
3368
3523
|
}
|
|
3369
3524
|
// src/services/provider-sessions.ts
|
|
3525
|
+
const TIME_LIMIT = 1e4;
|
|
3526
|
+
function timeLimit(work, message) {
|
|
3527
|
+
let timer;
|
|
3528
|
+
return Promise.race([work, new Promise((_, reject) => {
|
|
3529
|
+
timer = setTimeout(() => reject(new Error(message)), TIME_LIMIT);
|
|
3530
|
+
})]).finally(() => clearTimeout(timer));
|
|
3531
|
+
}
|
|
3370
3532
|
async function info(provider, id, paths) {
|
|
3371
3533
|
switch (provider) {
|
|
3372
3534
|
case "claude": return (await Promise.all([...paths].map(async ([cwd, projectId]) => {
|
|
@@ -3450,7 +3612,6 @@ async function find(provider, paths) {
|
|
|
3450
3612
|
}
|
|
3451
3613
|
// src/services/sessions.ts
|
|
3452
3614
|
const day = 864e5;
|
|
3453
|
-
const LIST_TIMEOUT = 1e4;
|
|
3454
3615
|
const REFRESH_DELAY = 1e3;
|
|
3455
3616
|
const allProviders = [
|
|
3456
3617
|
"claude",
|
|
@@ -3490,13 +3651,34 @@ function sessions() {
|
|
|
3490
3651
|
async function inactiveSessions(projectId) {
|
|
3491
3652
|
return (await everything(projectPaths().filter(([, id]) => projectId === null || id === projectId))).filter((session) => session.status === "inactive").sort((a, b) => b.updatedAt - a.updatedAt);
|
|
3492
3653
|
}
|
|
3493
|
-
async function
|
|
3494
|
-
const
|
|
3495
|
-
const
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3654
|
+
async function searchableSessions() {
|
|
3655
|
+
const paths = projectPaths();
|
|
3656
|
+
const lists = await Promise.all(enabledProviders().map(async (provider) => {
|
|
3657
|
+
try {
|
|
3658
|
+
return {
|
|
3659
|
+
provider,
|
|
3660
|
+
items: await timeLimit(find(provider, new Map(paths)), `${providerName(provider)} did not list its sessions in time`) ?? [],
|
|
3661
|
+
failed: false
|
|
3662
|
+
};
|
|
3663
|
+
} catch (error) {
|
|
3664
|
+
console.warn(`could not search ${providerName(provider)}: ${error}`);
|
|
3665
|
+
return {
|
|
3666
|
+
provider,
|
|
3667
|
+
items: [],
|
|
3668
|
+
failed: true
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
}));
|
|
3672
|
+
const found = lists.flatMap((list) => list.items);
|
|
3673
|
+
for (const item of found) providers.set(item.id, item.provider);
|
|
3674
|
+
const opens = new Map(db.select().from(sessionOpens).all().map((record) => [record.sessionId, record]));
|
|
3675
|
+
return {
|
|
3676
|
+
sessions: withStatus(found).map((session) => ({
|
|
3677
|
+
...session,
|
|
3678
|
+
opens: opens.get(session.id) ?? null
|
|
3679
|
+
})),
|
|
3680
|
+
failed: lists.filter((list) => list.failed).map((list) => list.provider)
|
|
3681
|
+
};
|
|
3500
3682
|
}
|
|
3501
3683
|
async function findSession(id) {
|
|
3502
3684
|
const paths = new Map(projectPaths());
|
|
@@ -3516,7 +3698,7 @@ async function findSession(id) {
|
|
|
3516
3698
|
return null;
|
|
3517
3699
|
}
|
|
3518
3700
|
async function everything(paths) {
|
|
3519
|
-
const found = (await Promise.all(
|
|
3701
|
+
const found = (await Promise.all(enabledProviders().map((provider) => find(provider, new Map(paths))))).flatMap((items) => items ?? []);
|
|
3520
3702
|
for (const item of found) providers.set(item.id, item.provider);
|
|
3521
3703
|
return withStatus(found);
|
|
3522
3704
|
}
|
|
@@ -3532,6 +3714,9 @@ function withStatus(items) {
|
|
|
3532
3714
|
};
|
|
3533
3715
|
});
|
|
3534
3716
|
}
|
|
3717
|
+
function enabledProviders() {
|
|
3718
|
+
return allProviders.filter((provider) => readConfig().enabledProviders[provider]);
|
|
3719
|
+
}
|
|
3535
3720
|
function projectPaths() {
|
|
3536
3721
|
return projects().map((project) => [project.path, project.id]);
|
|
3537
3722
|
}
|
|
@@ -3562,6 +3747,16 @@ function recordInteraction(id) {
|
|
|
3562
3747
|
}
|
|
3563
3748
|
}).run();
|
|
3564
3749
|
}
|
|
3750
|
+
function recordOpen(id, query) {
|
|
3751
|
+
const opens = nextOpens(db.select().from(sessionOpens).where(eq(sessionOpens.sessionId, id)).get() ?? null, Date.now(), query);
|
|
3752
|
+
db.insert(sessionOpens).values({
|
|
3753
|
+
sessionId: id,
|
|
3754
|
+
...opens
|
|
3755
|
+
}).onConflictDoUpdate({
|
|
3756
|
+
target: sessionOpens.sessionId,
|
|
3757
|
+
set: opens
|
|
3758
|
+
}).run();
|
|
3759
|
+
}
|
|
3565
3760
|
function publishSessions() {
|
|
3566
3761
|
broadcast({
|
|
3567
3762
|
type: "sessions",
|
|
@@ -3595,7 +3790,6 @@ async function refresh(provider) {
|
|
|
3595
3790
|
async function list(provider) {
|
|
3596
3791
|
const paths = new Map(projectPaths());
|
|
3597
3792
|
const before = statuses[provider];
|
|
3598
|
-
let timer;
|
|
3599
3793
|
try {
|
|
3600
3794
|
if (!readConfig().enabledProviders[provider]) {
|
|
3601
3795
|
listed[provider] = [];
|
|
@@ -3606,9 +3800,7 @@ async function list(provider) {
|
|
|
3606
3800
|
};
|
|
3607
3801
|
return;
|
|
3608
3802
|
}
|
|
3609
|
-
const found = await
|
|
3610
|
-
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${providerName(provider)} did not list its sessions within ${LIST_TIMEOUT / 1e3} seconds`)), LIST_TIMEOUT);
|
|
3611
|
-
})]);
|
|
3803
|
+
const found = await timeLimit(tracked(provider, paths), `${providerName(provider)} did not list its sessions in time`);
|
|
3612
3804
|
if (found === null) {
|
|
3613
3805
|
listed[provider] = [];
|
|
3614
3806
|
statuses[provider] = {
|
|
@@ -3634,7 +3826,6 @@ async function list(provider) {
|
|
|
3634
3826
|
error: message
|
|
3635
3827
|
};
|
|
3636
3828
|
} finally {
|
|
3637
|
-
clearTimeout(timer);
|
|
3638
3829
|
publishSessions();
|
|
3639
3830
|
if (JSON.stringify(statuses[provider]) !== JSON.stringify(before)) broadcast({
|
|
3640
3831
|
type: "providers",
|
|
@@ -3722,6 +3913,7 @@ async function removeSession(id) {
|
|
|
3722
3913
|
case "opencode": await removeOpenCode(id);
|
|
3723
3914
|
}
|
|
3724
3915
|
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
3916
|
+
db.delete(sessionOpens).where(eq(sessionOpens.sessionId, id)).run();
|
|
3725
3917
|
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
3726
3918
|
}
|
|
3727
3919
|
async function fork(sessionId, projectId, messageId) {
|
|
@@ -5354,6 +5546,140 @@ var Run = class {
|
|
|
5354
5546
|
});
|
|
5355
5547
|
}
|
|
5356
5548
|
};
|
|
5549
|
+
// src/services/message-search.ts
|
|
5550
|
+
const READ_LIMIT = 30;
|
|
5551
|
+
const CODEX_READ_LIMIT = 10;
|
|
5552
|
+
const RG_TIME_LIMIT = 5e3;
|
|
5553
|
+
const generatedText = /^(?:This session is being continued from a previous conversation|<command-|<local-command-|<bash-|<task-notification|<cross-session-message|Another Claude session sent a message:|\[Request interrupted by user)/;
|
|
5554
|
+
const run$2 = promisify(execFile);
|
|
5555
|
+
async function searchMessages(query) {
|
|
5556
|
+
if (!hasSearchableWord(query)) return {
|
|
5557
|
+
matches: [],
|
|
5558
|
+
failed: []
|
|
5559
|
+
};
|
|
5560
|
+
const paths = new Map(projects().map((project) => [project.path, project.id]));
|
|
5561
|
+
const folders = new Map(projects().map((project) => [project.id, project.path]));
|
|
5562
|
+
const results = await Promise.all([
|
|
5563
|
+
"claude",
|
|
5564
|
+
"codex",
|
|
5565
|
+
"opencode"
|
|
5566
|
+
].filter((provider) => readConfig().enabledProviders[provider]).map(async (provider) => {
|
|
5567
|
+
try {
|
|
5568
|
+
return {
|
|
5569
|
+
provider,
|
|
5570
|
+
matches: await timeLimit(search(provider, query, paths, folders), `${providerName(provider)} did not search in time`),
|
|
5571
|
+
failed: false
|
|
5572
|
+
};
|
|
5573
|
+
} catch (error) {
|
|
5574
|
+
console.warn(`could not search ${providerName(provider)} messages: ${error}`);
|
|
5575
|
+
return {
|
|
5576
|
+
provider,
|
|
5577
|
+
matches: [],
|
|
5578
|
+
failed: true
|
|
5579
|
+
};
|
|
5580
|
+
}
|
|
5581
|
+
}));
|
|
5582
|
+
return {
|
|
5583
|
+
matches: results.flatMap((result) => result.matches),
|
|
5584
|
+
failed: results.filter((result) => result.failed).map((result) => result.provider)
|
|
5585
|
+
};
|
|
5586
|
+
}
|
|
5587
|
+
async function search(provider, query, paths, folders) {
|
|
5588
|
+
const found = await find(provider, paths);
|
|
5589
|
+
if (found === null) return [];
|
|
5590
|
+
const sessions = newestFirst(found);
|
|
5591
|
+
switch (provider) {
|
|
5592
|
+
case "claude": return searchClaude(query, sessions, folders);
|
|
5593
|
+
case "codex": return searchCodex(query, sessions);
|
|
5594
|
+
case "opencode": return readAndMatch(query, sessions.slice(0, READ_LIMIT), openCodeMessages);
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
function newestFirst(sessions) {
|
|
5598
|
+
return [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
5599
|
+
}
|
|
5600
|
+
async function searchClaude(query, sessions, folders) {
|
|
5601
|
+
const candidates = await claudeCandidates(query);
|
|
5602
|
+
const matching = candidates === null ? sessions : sessions.filter((session) => candidates.has(session.id));
|
|
5603
|
+
const projectOf = new Map(sessions.map((session) => [session.id, session.projectId]));
|
|
5604
|
+
return readAndMatch(query, matching.slice(0, READ_LIMIT), (sessionId) => claudeMessages(sessionId, folders.get(projectOf.get(sessionId) ?? "")));
|
|
5605
|
+
}
|
|
5606
|
+
async function claudeMessages(sessionId, dir) {
|
|
5607
|
+
return (await getSessionMessages(sessionId, { dir })).flatMap((item) => {
|
|
5608
|
+
if (item.type === "system" || item.parent_tool_use_id !== null) return [];
|
|
5609
|
+
const text = toMessage(item.type, item, null).content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
|
|
5610
|
+
return generatedText.test(text) ? [] : [{
|
|
5611
|
+
author: item.type,
|
|
5612
|
+
text
|
|
5613
|
+
}];
|
|
5614
|
+
});
|
|
5615
|
+
}
|
|
5616
|
+
async function claudeCandidates(query) {
|
|
5617
|
+
const words = searchWords(query).filter((word) => word.length >= 3 && !/["\\]/.test(word));
|
|
5618
|
+
if (words.length === 0) return null;
|
|
5619
|
+
try {
|
|
5620
|
+
const lists = await Promise.all(words.map((word) => sessionsWith(word)));
|
|
5621
|
+
return new Set(lists[0].filter((id) => lists.every((list) => list.includes(id))));
|
|
5622
|
+
} catch (error) {
|
|
5623
|
+
console.warn(`could not narrow Claude sessions with rg: ${error}`);
|
|
5624
|
+
return null;
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
async function sessionsWith(word) {
|
|
5628
|
+
return (await run$2("rg", [
|
|
5629
|
+
"--files-with-matches",
|
|
5630
|
+
"--fixed-strings",
|
|
5631
|
+
"--ignore-case",
|
|
5632
|
+
"--no-ignore",
|
|
5633
|
+
"--hidden",
|
|
5634
|
+
"--max-depth",
|
|
5635
|
+
"2",
|
|
5636
|
+
"--glob",
|
|
5637
|
+
"*.jsonl",
|
|
5638
|
+
"--",
|
|
5639
|
+
word,
|
|
5640
|
+
path.join(claudeBaseDir(), "projects")
|
|
5641
|
+
], {
|
|
5642
|
+
maxBuffer: 16777216,
|
|
5643
|
+
timeout: RG_TIME_LIMIT
|
|
5644
|
+
}).catch((error) => {
|
|
5645
|
+
if (error.code === 1) return { stdout: "" };
|
|
5646
|
+
throw error;
|
|
5647
|
+
})).stdout.split("\n").filter(Boolean).map((file) => path.basename(file, ".jsonl"));
|
|
5648
|
+
}
|
|
5649
|
+
async function searchCodex(query, sessions) {
|
|
5650
|
+
const longest = [...searchWords(query)].sort((a, b) => b.length - a.length)[0];
|
|
5651
|
+
const known = new Map(sessions.map((session) => [session.id, session]));
|
|
5652
|
+
const found = (await codexSearch(longest)).filter((result) => known.has(result.thread.id)).sort((a, b) => b.thread.updatedAt - a.thread.updatedAt);
|
|
5653
|
+
const verified = await readAndMatch(query, found.slice(0, CODEX_READ_LIMIT).map((result) => ({ id: result.thread.id })), codexMessages);
|
|
5654
|
+
const unverified = found.slice(CODEX_READ_LIMIT).flatMap((result) => {
|
|
5655
|
+
const match = matchMessage(query, result.snippet);
|
|
5656
|
+
return match === null ? [] : [{
|
|
5657
|
+
sessionId: result.thread.id,
|
|
5658
|
+
author: "assistant",
|
|
5659
|
+
quality: match.quality,
|
|
5660
|
+
snippet: messageSnippet(result.snippet, match.offset)
|
|
5661
|
+
}];
|
|
5662
|
+
});
|
|
5663
|
+
return [...verified, ...unverified];
|
|
5664
|
+
}
|
|
5665
|
+
async function readAndMatch(query, sessions, read) {
|
|
5666
|
+
return (await Promise.all(sessions.map(async (session) => bestMatch(query, session.id, await read(session.id))))).filter((match) => match !== null);
|
|
5667
|
+
}
|
|
5668
|
+
function bestMatch(query, sessionId, messages) {
|
|
5669
|
+
let best = null;
|
|
5670
|
+
for (const message of messages) {
|
|
5671
|
+
const match = matchMessage(query, message.text);
|
|
5672
|
+
if (match === null) continue;
|
|
5673
|
+
const candidate = {
|
|
5674
|
+
sessionId,
|
|
5675
|
+
author: message.author,
|
|
5676
|
+
quality: match.quality,
|
|
5677
|
+
snippet: messageSnippet(message.text, match.offset)
|
|
5678
|
+
};
|
|
5679
|
+
if (best === null || compareMessages(candidate, best) < 0) best = candidate;
|
|
5680
|
+
}
|
|
5681
|
+
return best;
|
|
5682
|
+
}
|
|
5357
5683
|
// src/services/proxy.ts
|
|
5358
5684
|
const proxies = /* @__PURE__ */ new Map();
|
|
5359
5685
|
const pageScriptPath = "/__offsite/browser.js";
|
|
@@ -7008,10 +7334,12 @@ const handlers = {
|
|
|
7008
7334
|
"sessions.open": async (params, client) => {
|
|
7009
7335
|
const session = await findSession(params.sessionId);
|
|
7010
7336
|
await open$1(client, params.sessionId, params.projectId);
|
|
7337
|
+
if (params.opened && session !== null) recordOpen(params.sessionId, params.query);
|
|
7011
7338
|
return session;
|
|
7012
7339
|
},
|
|
7013
7340
|
"sessions.inactive": (params) => inactiveSessions(params.projectId),
|
|
7014
|
-
"sessions.
|
|
7341
|
+
"sessions.searchable": () => searchableSessions(),
|
|
7342
|
+
"sessions.searchMessages": (params) => searchMessages(params.query),
|
|
7015
7343
|
"sessions.earlier": (params) => earlier(params.sessionId, params.before),
|
|
7016
7344
|
"sessions.image": (params) => image(params.sessionId, params.itemId, params.index),
|
|
7017
7345
|
"sessions.agent": (params) => {
|