@a-fig/accordion 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion.js +1049 -0
- package/dist/client/_app/immutable/assets/0.D1oHPnFb.css +1 -0
- package/dist/client/_app/immutable/assets/2.DovUm2YN.css +1 -0
- package/dist/client/_app/immutable/assets/ConductorDashboard.Bj2_AmkQ.css +1 -0
- package/dist/client/_app/immutable/assets/conductorDiagnostics.Ci61O58E.css +1 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-cyrillic-400-normal.BSMlKf0J.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-cyrillic-400-normal.CEL4l2ZJ.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-cyrillic-500-normal.Ael50iVv.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-cyrillic-500-normal.Bq9vWWag.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-400-normal.DMdlQ8Kv.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-400-normal.xuaO2J-f.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-500-normal.BIfNGwUT.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-500-normal.BqneJy0T.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-latin-ext-400-normal.BmRBH3aV.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-latin-ext-400-normal.D3D2R8hC.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-latin-ext-500-normal.CAhNIIs5.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-latin-ext-500-normal.CZ70TYgx.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-vietnamese-400-normal.BulugwFq.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-vietnamese-400-normal.DDuiU_S-.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-vietnamese-500-normal.C8zxqsMH.woff +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-mono-vietnamese-500-normal.DZ4AoWbu.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-sans-cyrillic-ext-wght-normal.d45eAU9y.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-sans-cyrillic-wght-normal.BAAhND-U.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-sans-greek-wght-normal.CmyJS8uq.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-sans-latin-ext-wght-normal.CIII54If.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-sans-latin-wght-normal.IvpUvPa2.woff2 +0 -0
- package/dist/client/_app/immutable/assets/ibm-plex-sans-vietnamese-wght-normal.Dg1JeJN0.woff2 +0 -0
- package/dist/client/_app/immutable/chunks/4w40b9u5.js +1 -0
- package/dist/client/_app/immutable/chunks/BMYqN1jH.js +1 -0
- package/dist/client/_app/immutable/chunks/BO5jq8Rd.js +1 -0
- package/dist/client/_app/immutable/chunks/C32X_W8t.js +1 -0
- package/dist/client/_app/immutable/chunks/CGLlrrjc.js +2 -0
- package/dist/client/_app/immutable/chunks/Cho1IevM.js +1 -0
- package/dist/client/_app/immutable/chunks/CjuRsM1R.js +1 -0
- package/dist/client/_app/immutable/chunks/CmEYqMe3.js +1 -0
- package/dist/client/_app/immutable/chunks/D31MrvWc.js +1 -0
- package/dist/client/_app/immutable/chunks/D5WXxLoC.js +1 -0
- package/dist/client/_app/immutable/chunks/DP6xtK1Q.js +1 -0
- package/dist/client/_app/immutable/chunks/DVFVkhSS.js +1 -0
- package/dist/client/_app/immutable/chunks/DhEqZVGG.js +1 -0
- package/dist/client/_app/immutable/chunks/DjVbd_At.js +106 -0
- package/dist/client/_app/immutable/chunks/VcM4J_n8.js +1 -0
- package/dist/client/_app/immutable/chunks/_emMOgeR.js +1 -0
- package/dist/client/_app/immutable/entry/app.BhWfMl-z.js +2 -0
- package/dist/client/_app/immutable/entry/start.DeRcQ_-b.js +1 -0
- package/dist/client/_app/immutable/nodes/0.NhrX4SHq.js +1 -0
- package/dist/client/_app/immutable/nodes/1.yk89wyT9.js +1 -0
- package/dist/client/_app/immutable/nodes/2.DFJdFzQs.js +14 -0
- package/dist/client/_app/immutable/nodes/3.Np693YfE.js +1 -0
- package/dist/client/_app/immutable/nodes/4.RNp6Kzkx.js +1 -0
- package/dist/client/_app/version.json +1 -0
- package/dist/client/brand-symbol-spectrum.png +0 -0
- package/dist/client/brand-symbol.png +0 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/index.html +43 -0
- package/dist/client/sample-session.jsonl +520 -0
- package/dist/client/svelte.svg +1 -0
- package/dist/client/tauri.svg +6 -0
- package/dist/client/vite.svg +1 -0
- package/package.json +34 -0
- package/skills/accordion-context-folding/SKILL.md +53 -0
- package/skills/accordion-context-recall/SKILL.md +38 -0
package/accordion.js
ADDED
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
// accordion.ts
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as http from "node:http";
|
|
7
|
+
import * as crypto from "node:crypto";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
|
|
12
|
+
// ../app/src/lib/engine/tokens.ts
|
|
13
|
+
var CHARS_PER_TOKEN = 4;
|
|
14
|
+
var BLOCK_OVERHEAD = 4;
|
|
15
|
+
function estTokens(s) {
|
|
16
|
+
if (!s) return 0;
|
|
17
|
+
return Math.ceil(s.length / CHARS_PER_TOKEN);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ../app/src/lib/live/mapping.ts
|
|
21
|
+
function blockId(m, i, partIndex) {
|
|
22
|
+
switch (m.role) {
|
|
23
|
+
case "user":
|
|
24
|
+
return m.timestamp != null ? `u:${m.timestamp}` : `m${i}:u`;
|
|
25
|
+
case "assistant": {
|
|
26
|
+
if (partIndex == null) return `m${i}:p?`;
|
|
27
|
+
const anchor = m.responseId != null ? m.responseId : m.timestamp != null ? `t${m.timestamp}` : null;
|
|
28
|
+
return anchor != null ? `a:${anchor}:p${partIndex}` : `m${i}:p${partIndex}`;
|
|
29
|
+
}
|
|
30
|
+
case "toolResult":
|
|
31
|
+
return m.toolCallId != null ? `r:${m.toolCallId}` : `m${i}:r`;
|
|
32
|
+
default:
|
|
33
|
+
return m.timestamp != null ? `s:${m.timestamp}` : `m${i}:s`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function isDurableId(id) {
|
|
37
|
+
return id.startsWith("u:") || id.startsWith("a:") || id.startsWith("r:") || id.startsWith("s:");
|
|
38
|
+
}
|
|
39
|
+
function textOf(content) {
|
|
40
|
+
if (typeof content === "string") return content;
|
|
41
|
+
if (Array.isArray(content))
|
|
42
|
+
return content.filter((b) => !!b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
var tokensFor = (text) => estTokens(text) + BLOCK_OVERHEAD;
|
|
46
|
+
function linearize(messages) {
|
|
47
|
+
const out = [];
|
|
48
|
+
let order = 0;
|
|
49
|
+
let turn = 0;
|
|
50
|
+
const push = (id, kind, text, extra = {}) => {
|
|
51
|
+
if (!text && kind !== "tool_result") return;
|
|
52
|
+
out.push({ id, kind, turn, order: order++, text, tokens: tokensFor(text), ...extra });
|
|
53
|
+
};
|
|
54
|
+
messages.forEach((m, i) => {
|
|
55
|
+
switch (m.role) {
|
|
56
|
+
case "user": {
|
|
57
|
+
turn += 1;
|
|
58
|
+
push(blockId(m, i), "user", textOf(m.content));
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "assistant": {
|
|
62
|
+
const parts = Array.isArray(m.content) ? m.content : [];
|
|
63
|
+
parts.forEach((b, j) => {
|
|
64
|
+
if (b?.type === "thinking") push(blockId(m, i, j), "thinking", b.thinking || "", { model: m.model });
|
|
65
|
+
else if (b?.type === "text") push(blockId(m, i, j), "text", b.text || "", { model: m.model });
|
|
66
|
+
else if (b?.type === "toolCall") {
|
|
67
|
+
const c = b;
|
|
68
|
+
push(blockId(m, i, j), "tool_call", `${c.name} ${JSON.stringify(c.arguments ?? {})}`, {
|
|
69
|
+
toolName: c.name,
|
|
70
|
+
callId: c.id,
|
|
71
|
+
model: m.model
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "toolResult": {
|
|
78
|
+
push(blockId(m, i), "tool_result", textOf(m.content), {
|
|
79
|
+
toolName: m.toolName || "tool",
|
|
80
|
+
callId: m.toolCallId,
|
|
81
|
+
isError: !!m.isError
|
|
82
|
+
});
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
default: {
|
|
86
|
+
if (typeof m.summary === "string" && m.summary) push(blockId(m, i), "text", m.summary);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function messageInfo(m, i) {
|
|
93
|
+
const ids = [];
|
|
94
|
+
const calls = [];
|
|
95
|
+
const results = [];
|
|
96
|
+
let hasNonDurable = false;
|
|
97
|
+
const push = (id) => {
|
|
98
|
+
ids.push(id);
|
|
99
|
+
if (!isDurableId(id)) hasNonDurable = true;
|
|
100
|
+
};
|
|
101
|
+
switch (m.role) {
|
|
102
|
+
case "user":
|
|
103
|
+
push(blockId(m, i));
|
|
104
|
+
break;
|
|
105
|
+
case "assistant": {
|
|
106
|
+
const parts = Array.isArray(m.content) ? m.content : [];
|
|
107
|
+
parts.forEach((b, j) => {
|
|
108
|
+
if (b?.type === "thinking") {
|
|
109
|
+
if (b.thinking) push(blockId(m, i, j));
|
|
110
|
+
} else if (b?.type === "text") {
|
|
111
|
+
if (b.text) push(blockId(m, i, j));
|
|
112
|
+
} else if (b?.type === "toolCall") {
|
|
113
|
+
push(blockId(m, i, j));
|
|
114
|
+
const id = b.id;
|
|
115
|
+
if (id) calls.push(id);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "toolResult":
|
|
121
|
+
push(blockId(m, i));
|
|
122
|
+
if (m.toolCallId) results.push(m.toolCallId);
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
if (typeof m.summary === "string" && m.summary) push(blockId(m, i));
|
|
126
|
+
}
|
|
127
|
+
return { ids, calls, results, hasNonDurable };
|
|
128
|
+
}
|
|
129
|
+
function foldOne(m, i, byId, mark) {
|
|
130
|
+
if (m.role === "assistant" && Array.isArray(m.content)) {
|
|
131
|
+
let parts = null;
|
|
132
|
+
m.content.forEach((b, j) => {
|
|
133
|
+
const op = byId.get(blockId(m, i, j));
|
|
134
|
+
if (!op || !op.digestText) return;
|
|
135
|
+
if (b?.type === "text") {
|
|
136
|
+
parts ??= m.content.slice();
|
|
137
|
+
parts[j] = { ...b, text: op.digestText };
|
|
138
|
+
} else if (b?.type === "thinking") {
|
|
139
|
+
parts ??= m.content.slice();
|
|
140
|
+
parts[j] = { ...b, thinking: op.digestText };
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (parts) {
|
|
144
|
+
mark();
|
|
145
|
+
return { ...m, content: parts };
|
|
146
|
+
}
|
|
147
|
+
return m;
|
|
148
|
+
}
|
|
149
|
+
if (m.role === "toolResult") {
|
|
150
|
+
const op = byId.get(blockId(m, i));
|
|
151
|
+
if (op && op.digestText) {
|
|
152
|
+
mark();
|
|
153
|
+
return { ...m, content: [{ type: "text", text: op.digestText }] };
|
|
154
|
+
}
|
|
155
|
+
return m;
|
|
156
|
+
}
|
|
157
|
+
return m;
|
|
158
|
+
}
|
|
159
|
+
function applyPlan(messages, ops, groups = []) {
|
|
160
|
+
const safeOps = (ops ?? []).filter((o) => o && typeof o.id === "string" && isDurableId(o.id) && typeof o.digestText === "string" && o.digestText);
|
|
161
|
+
const safeGroups = (groups ?? []).filter(
|
|
162
|
+
(g) => g && Array.isArray(g.memberIds) && g.memberIds.length && g.memberIds.every((m) => typeof m === "string") && (g.summaryText === null || typeof g.summaryText === "string" && g.summaryText.trim())
|
|
163
|
+
);
|
|
164
|
+
if (!safeOps.length && !safeGroups.length) return messages;
|
|
165
|
+
const byId = new Map(safeOps.map((o) => [o.id, o]));
|
|
166
|
+
const owner = new Array(messages.length).fill(null);
|
|
167
|
+
if (safeGroups.length) {
|
|
168
|
+
const memberToGroup = /* @__PURE__ */ new Map();
|
|
169
|
+
for (const g of safeGroups) for (const id of g.memberIds) if (isDurableId(id)) memberToGroup.set(id, g);
|
|
170
|
+
const infos = messages.map((m, i) => messageInfo(m, i));
|
|
171
|
+
for (let i = 0; i < messages.length; i++) {
|
|
172
|
+
const info = infos[i];
|
|
173
|
+
if (!info.ids.length || info.hasNonDurable) continue;
|
|
174
|
+
let g = null;
|
|
175
|
+
let ok = true;
|
|
176
|
+
for (const id of info.ids) {
|
|
177
|
+
const gg = memberToGroup.get(id);
|
|
178
|
+
if (!gg || g && gg !== g) {
|
|
179
|
+
ok = false;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
g = gg;
|
|
183
|
+
}
|
|
184
|
+
if (ok && g) owner[i] = g;
|
|
185
|
+
}
|
|
186
|
+
for (let changedSet = true; changedSet; ) {
|
|
187
|
+
changedSet = false;
|
|
188
|
+
const calls = /* @__PURE__ */ new Set();
|
|
189
|
+
const results = /* @__PURE__ */ new Set();
|
|
190
|
+
for (let i = 0; i < messages.length; i++) {
|
|
191
|
+
if (!owner[i]) continue;
|
|
192
|
+
for (const c of infos[i].calls) calls.add(c);
|
|
193
|
+
for (const c of infos[i].results) results.add(c);
|
|
194
|
+
}
|
|
195
|
+
for (let i = 0; i < messages.length; i++) {
|
|
196
|
+
if (!owner[i]) continue;
|
|
197
|
+
const info = infos[i];
|
|
198
|
+
if (info.calls.some((c) => !results.has(c)) || info.results.some((c) => !calls.has(c))) {
|
|
199
|
+
owner[i] = null;
|
|
200
|
+
changedSet = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
let changed = false;
|
|
206
|
+
const mark = () => {
|
|
207
|
+
changed = true;
|
|
208
|
+
};
|
|
209
|
+
const out = [];
|
|
210
|
+
for (let i = 0; i < messages.length; ) {
|
|
211
|
+
const g = owner[i];
|
|
212
|
+
if (g) {
|
|
213
|
+
let j = i + 1;
|
|
214
|
+
while (j < messages.length && owner[j] === g) j++;
|
|
215
|
+
if (g.summaryText === null) {
|
|
216
|
+
changed = true;
|
|
217
|
+
} else {
|
|
218
|
+
const role = messages[i].role === "assistant" ? "assistant" : "user";
|
|
219
|
+
out.push({ role, content: [{ type: "text", text: g.summaryText }] });
|
|
220
|
+
changed = true;
|
|
221
|
+
}
|
|
222
|
+
i = j;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
out.push(foldOne(messages[i], i, byId, mark));
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
return changed ? out : messages;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ../app/src/lib/live/protocol.ts
|
|
232
|
+
var PROTOCOL_VERSION = 5;
|
|
233
|
+
var DEFAULT_PORT = 4317;
|
|
234
|
+
|
|
235
|
+
// ../app/src/lib/live/registry.ts
|
|
236
|
+
var REGISTRY_PROTOCOL = 1;
|
|
237
|
+
var REGISTRY_DIR = ".accordion";
|
|
238
|
+
var SESSIONS_SUBDIR = "sessions";
|
|
239
|
+
var FOCUS_FILE = "focus.json";
|
|
240
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
241
|
+
|
|
242
|
+
// accordion.ts
|
|
243
|
+
var REQUEST_TIMEOUT_MS = 250;
|
|
244
|
+
var UNFOLD_TIMEOUT_MS = 2e3;
|
|
245
|
+
var RECALL_TIMEOUT_MS = 2e3;
|
|
246
|
+
var HOME = process.env.ACCORDION_HOME || os.homedir();
|
|
247
|
+
var REGISTRY_ROOT = path.join(HOME, REGISTRY_DIR);
|
|
248
|
+
var SESSIONS_DIR = path.join(REGISTRY_ROOT, SESSIONS_SUBDIR);
|
|
249
|
+
var FOCUS_PATH = path.join(REGISTRY_ROOT, FOCUS_FILE);
|
|
250
|
+
var ACCORDION_APP_FLAG = "accordion-app";
|
|
251
|
+
var ACCORDION_APP_ENV = "ACCORDION_APP_PATH";
|
|
252
|
+
function cleanExplicitPath(value) {
|
|
253
|
+
if (typeof value !== "string") return null;
|
|
254
|
+
let s = value.trim();
|
|
255
|
+
if (!s) return null;
|
|
256
|
+
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) s = s.slice(1, -1).trim();
|
|
257
|
+
if (s === "~") return os.homedir();
|
|
258
|
+
if (s.startsWith("~/") || s.startsWith("~\\")) return path.join(os.homedir(), s.slice(2));
|
|
259
|
+
return s;
|
|
260
|
+
}
|
|
261
|
+
function isLaunchableFile(p) {
|
|
262
|
+
try {
|
|
263
|
+
return fs.statSync(p).isFile();
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function windowsInstallCandidates() {
|
|
269
|
+
if (process.platform !== "win32") return [];
|
|
270
|
+
const roots = [
|
|
271
|
+
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, "Programs", "Accordion"),
|
|
272
|
+
process.env.ProgramFiles && path.join(process.env.ProgramFiles, "Accordion"),
|
|
273
|
+
process.env["ProgramFiles(x86)"] && path.join(process.env["ProgramFiles(x86)"], "Accordion"),
|
|
274
|
+
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, "Accordion")
|
|
275
|
+
].filter((s) => !!s);
|
|
276
|
+
const names = ["Accordion.exe", "app.exe"];
|
|
277
|
+
const out = [];
|
|
278
|
+
for (const root of roots) for (const name of names) out.push(path.join(root, name));
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
function repoAppCandidates() {
|
|
282
|
+
try {
|
|
283
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
284
|
+
const repo = path.resolve(here, "..");
|
|
285
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
286
|
+
return [
|
|
287
|
+
path.join(repo, "app", "src-tauri", "target", "release", `app${ext}`),
|
|
288
|
+
path.join(repo, "app", "src-tauri", "target", "debug", `app${ext}`)
|
|
289
|
+
];
|
|
290
|
+
} catch {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function resolveAccordionApp(pi) {
|
|
295
|
+
const flagPath = cleanExplicitPath(pi.getFlag(ACCORDION_APP_FLAG));
|
|
296
|
+
if (flagPath) {
|
|
297
|
+
if (isLaunchableFile(flagPath)) return { ok: true, path: flagPath, source: "cli" };
|
|
298
|
+
return { ok: false, reason: "explicit-invalid", path: flagPath, source: "cli" };
|
|
299
|
+
}
|
|
300
|
+
const envPath = cleanExplicitPath(process.env[ACCORDION_APP_ENV]);
|
|
301
|
+
if (envPath) {
|
|
302
|
+
if (isLaunchableFile(envPath)) return { ok: true, path: envPath, source: "env" };
|
|
303
|
+
return { ok: false, reason: "explicit-invalid", path: envPath, source: "env" };
|
|
304
|
+
}
|
|
305
|
+
for (const candidate of [...windowsInstallCandidates(), ...repoAppCandidates()]) {
|
|
306
|
+
if (isLaunchableFile(candidate)) return { ok: true, path: candidate, source: "default" };
|
|
307
|
+
}
|
|
308
|
+
return { ok: false, reason: "not-found" };
|
|
309
|
+
}
|
|
310
|
+
async function launchAccordionApp(pi) {
|
|
311
|
+
const resolved = resolveAccordionApp(pi);
|
|
312
|
+
if (!resolved.ok) return resolved;
|
|
313
|
+
try {
|
|
314
|
+
const child = spawn(resolved.path, [], { detached: true, stdio: "ignore", shell: false });
|
|
315
|
+
return await new Promise((resolve2) => {
|
|
316
|
+
let settled = false;
|
|
317
|
+
const ok = { ok: true, path: resolved.path, source: resolved.source };
|
|
318
|
+
const timer = setTimeout(() => finish(ok), 150);
|
|
319
|
+
const finish = (result) => {
|
|
320
|
+
if (settled) return;
|
|
321
|
+
settled = true;
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
child.off("spawn", onSpawn);
|
|
324
|
+
child.unref();
|
|
325
|
+
resolve2(result);
|
|
326
|
+
};
|
|
327
|
+
const onSpawn = () => finish(ok);
|
|
328
|
+
const onError = (error) => finish({ ok: false, reason: "spawn-failed", path: resolved.path, source: resolved.source, error });
|
|
329
|
+
child.once("spawn", onSpawn);
|
|
330
|
+
child.once("error", onError);
|
|
331
|
+
});
|
|
332
|
+
} catch (error) {
|
|
333
|
+
return { ok: false, reason: "spawn-failed", path: resolved.path, source: resolved.source, error };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function launchResultLine(result) {
|
|
337
|
+
if (!result) return { text: "Accordion focus requested for this session.", type: "info" };
|
|
338
|
+
if (result.ok) return { text: "Launching/focusing Accordion for this session\u2026", type: "info" };
|
|
339
|
+
if (result.reason === "explicit-invalid") {
|
|
340
|
+
const source = result.source === "cli" ? `--${ACCORDION_APP_FLAG}` : ACCORDION_APP_ENV;
|
|
341
|
+
return {
|
|
342
|
+
text: `Accordion focus request written, but ${source} does not point to an executable: ${result.path}`,
|
|
343
|
+
type: "warning"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (result.reason === "spawn-failed") {
|
|
347
|
+
return {
|
|
348
|
+
text: `Accordion focus request written, but launching failed for ${result.path}. Set ${ACCORDION_APP_ENV} or --${ACCORDION_APP_FLAG} to the Accordion executable.`,
|
|
349
|
+
type: "warning"
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
text: `Accordion focus request written, but I couldn't find the desktop app. Open Accordion manually, or set ${ACCORDION_APP_ENV} / --${ACCORDION_APP_FLAG}.`,
|
|
354
|
+
type: "warning"
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function accordionLive(pi) {
|
|
358
|
+
pi.registerFlag(ACCORDION_APP_FLAG, {
|
|
359
|
+
description: "Path to the Accordion desktop app executable for /accordion launch/focus",
|
|
360
|
+
type: "string"
|
|
361
|
+
});
|
|
362
|
+
let wss = null;
|
|
363
|
+
let httpServer = null;
|
|
364
|
+
let webToken = "";
|
|
365
|
+
let client = null;
|
|
366
|
+
let sessionId = "";
|
|
367
|
+
let meta = { title: "pi session", cwd: "", model: "", contextWindow: null, format: "pi" };
|
|
368
|
+
let sentCount = 0;
|
|
369
|
+
let reqSeq = 0;
|
|
370
|
+
let epoch = 0;
|
|
371
|
+
const pending = /* @__PURE__ */ new Map();
|
|
372
|
+
let unfoldSeq = 0;
|
|
373
|
+
const pendingUnfold = /* @__PURE__ */ new Map();
|
|
374
|
+
let recallSeq = 0;
|
|
375
|
+
const pendingRecall = /* @__PURE__ */ new Map();
|
|
376
|
+
let lastMessages = [];
|
|
377
|
+
let pendingSince = [];
|
|
378
|
+
let latestCtx = null;
|
|
379
|
+
let latestModel = null;
|
|
380
|
+
let port = 0;
|
|
381
|
+
let startedAt = 0;
|
|
382
|
+
let model = "";
|
|
383
|
+
let tokens = null;
|
|
384
|
+
let contextWindow = null;
|
|
385
|
+
let heartbeat = null;
|
|
386
|
+
const attached = () => !!client && client.readyState === 1;
|
|
387
|
+
function flushPending() {
|
|
388
|
+
for (const resolve2 of pending.values()) resolve2({ ops: [], groups: [] });
|
|
389
|
+
pending.clear();
|
|
390
|
+
for (const resolve2 of pendingUnfold.values()) resolve2(null);
|
|
391
|
+
pendingUnfold.clear();
|
|
392
|
+
for (const resolve2 of pendingRecall.values()) resolve2(null);
|
|
393
|
+
pendingRecall.clear();
|
|
394
|
+
}
|
|
395
|
+
function send(ws, m) {
|
|
396
|
+
try {
|
|
397
|
+
ws.send(JSON.stringify(m));
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function sendStream(frame) {
|
|
402
|
+
const ws = client;
|
|
403
|
+
if (!ws || ws.readyState !== 1) return;
|
|
404
|
+
send(ws, frame);
|
|
405
|
+
}
|
|
406
|
+
function buildEntry() {
|
|
407
|
+
return {
|
|
408
|
+
registryProtocol: REGISTRY_PROTOCOL,
|
|
409
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
410
|
+
sessionId,
|
|
411
|
+
port,
|
|
412
|
+
pid: process.pid,
|
|
413
|
+
cwd: meta.cwd,
|
|
414
|
+
title: meta.title,
|
|
415
|
+
model,
|
|
416
|
+
tokens,
|
|
417
|
+
contextWindow,
|
|
418
|
+
startedAt,
|
|
419
|
+
heartbeatAt: Date.now()
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function writeEntry() {
|
|
423
|
+
if (!port || !sessionId) return;
|
|
424
|
+
try {
|
|
425
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
426
|
+
const target = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
427
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
428
|
+
fs.writeFileSync(tmp, JSON.stringify(buildEntry()));
|
|
429
|
+
fs.renameSync(tmp, target);
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function deleteEntry() {
|
|
434
|
+
if (!sessionId) return;
|
|
435
|
+
try {
|
|
436
|
+
fs.unlinkSync(path.join(SESSIONS_DIR, `${sessionId}.json`));
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function writeFocusRequest() {
|
|
441
|
+
if (!sessionId) return;
|
|
442
|
+
try {
|
|
443
|
+
fs.mkdirSync(REGISTRY_ROOT, { recursive: true });
|
|
444
|
+
const req = { sessionId, ts: Date.now() };
|
|
445
|
+
const tmp = `${FOCUS_PATH}.${process.pid}.tmp`;
|
|
446
|
+
fs.writeFileSync(tmp, JSON.stringify(req));
|
|
447
|
+
fs.renameSync(tmp, FOCUS_PATH);
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function readSessionMessages(c) {
|
|
452
|
+
if (!c) return [];
|
|
453
|
+
let sm;
|
|
454
|
+
try {
|
|
455
|
+
sm = c.sessionManager;
|
|
456
|
+
} catch {
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
if (!sm) return [];
|
|
460
|
+
try {
|
|
461
|
+
const sc = sm.buildSessionContext?.();
|
|
462
|
+
if (sc && Array.isArray(sc.messages)) return sc.messages;
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const branch = sm.getBranch?.() ?? [];
|
|
467
|
+
const msgs = branch.filter((e) => e.type === "message" && e.message).map((e) => e.message);
|
|
468
|
+
msgs.reverse();
|
|
469
|
+
return msgs;
|
|
470
|
+
} catch {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function applyModel(m) {
|
|
475
|
+
if (!m) return;
|
|
476
|
+
latestModel = m;
|
|
477
|
+
if (m.id) {
|
|
478
|
+
model = m.id;
|
|
479
|
+
meta.model = m.id;
|
|
480
|
+
}
|
|
481
|
+
if (typeof m.contextWindow === "number" && m.contextWindow > 0) {
|
|
482
|
+
contextWindow = m.contextWindow;
|
|
483
|
+
meta.contextWindow = m.contextWindow;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function refreshFromCtx(ctx) {
|
|
487
|
+
try {
|
|
488
|
+
applyModel(ctx.model);
|
|
489
|
+
const u = ctx.getContextUsage?.();
|
|
490
|
+
if (u) {
|
|
491
|
+
tokens = u.tokens;
|
|
492
|
+
if (typeof u.contextWindow === "number") {
|
|
493
|
+
contextWindow = u.contextWindow;
|
|
494
|
+
meta.contextWindow = u.contextWindow;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const MIME = {
|
|
501
|
+
".html": "text/html; charset=utf-8",
|
|
502
|
+
".js": "text/javascript",
|
|
503
|
+
".mjs": "text/javascript",
|
|
504
|
+
".css": "text/css",
|
|
505
|
+
".json": "application/json",
|
|
506
|
+
".png": "image/png",
|
|
507
|
+
".svg": "image/svg+xml",
|
|
508
|
+
".ico": "image/x-icon",
|
|
509
|
+
".woff2": "font/woff2",
|
|
510
|
+
".woff": "font/woff",
|
|
511
|
+
".txt": "text/plain",
|
|
512
|
+
".map": "application/json"
|
|
513
|
+
};
|
|
514
|
+
function resolveClientRoot() {
|
|
515
|
+
try {
|
|
516
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
517
|
+
const candidates = [path.join(here, "dist", "client"), path.resolve(here, "..", "app", "build")];
|
|
518
|
+
for (const dir of candidates) {
|
|
519
|
+
try {
|
|
520
|
+
if (fs.statSync(dir).isDirectory()) return dir;
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
function isWebAuthed(req, u) {
|
|
529
|
+
if (!webToken) return false;
|
|
530
|
+
if (u.searchParams.get("token") === webToken) return true;
|
|
531
|
+
const cookie = req.headers["cookie"];
|
|
532
|
+
if (typeof cookie === "string" && cookie.split(";").some((c) => c.trim() === `accordion_token=${webToken}`)) return true;
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
function handleHttp(req, res) {
|
|
536
|
+
try {
|
|
537
|
+
const u = new URL(req.url || "/", "http://127.0.0.1");
|
|
538
|
+
if (u.pathname === "/__accordion/meta") {
|
|
539
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
540
|
+
res.end(JSON.stringify({ served: true, sessionId, protocolVersion: PROTOCOL_VERSION }));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (!isWebAuthed(req, u)) {
|
|
544
|
+
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
|
545
|
+
res.end("Forbidden \u2014 open Accordion via the /accordion command's Browser link (it carries the session token).");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const headers = {};
|
|
549
|
+
if (u.searchParams.get("token") === webToken) {
|
|
550
|
+
headers["Set-Cookie"] = `accordion_token=${webToken}; HttpOnly; SameSite=Strict; Path=/`;
|
|
551
|
+
}
|
|
552
|
+
const root = resolveClientRoot();
|
|
553
|
+
if (!root) {
|
|
554
|
+
res.writeHead(404, { ...headers, "Content-Type": "text/plain; charset=utf-8" });
|
|
555
|
+
res.end("No browser build found. Run `npm run build` in app/, or `npm run build:client` in extension/.");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
let rel = decodeURIComponent(u.pathname);
|
|
559
|
+
if (rel === "/") rel = "/index.html";
|
|
560
|
+
let filePath = path.join(root, rel);
|
|
561
|
+
const rootResolved = path.resolve(root);
|
|
562
|
+
if (path.resolve(filePath) !== rootResolved && !path.resolve(filePath).startsWith(rootResolved + path.sep)) {
|
|
563
|
+
res.writeHead(403, { ...headers, "Content-Type": "text/plain; charset=utf-8" });
|
|
564
|
+
res.end("Forbidden");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
let exists = false;
|
|
568
|
+
try {
|
|
569
|
+
exists = fs.statSync(filePath).isFile();
|
|
570
|
+
} catch {
|
|
571
|
+
exists = false;
|
|
572
|
+
}
|
|
573
|
+
if (!exists) {
|
|
574
|
+
if (path.extname(rel) === "") {
|
|
575
|
+
filePath = path.join(root, "index.html");
|
|
576
|
+
} else {
|
|
577
|
+
res.writeHead(404, { ...headers, "Content-Type": "text/plain; charset=utf-8" });
|
|
578
|
+
res.end("Not found");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
let body;
|
|
583
|
+
try {
|
|
584
|
+
body = fs.readFileSync(filePath);
|
|
585
|
+
} catch {
|
|
586
|
+
res.writeHead(404, { ...headers, "Content-Type": "text/plain; charset=utf-8" });
|
|
587
|
+
res.end("Not found");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const mime = MIME[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
591
|
+
res.writeHead(200, { ...headers, "Content-Type": mime });
|
|
592
|
+
res.end(body);
|
|
593
|
+
} catch {
|
|
594
|
+
try {
|
|
595
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
596
|
+
res.end("Internal error");
|
|
597
|
+
} catch {
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function startServer() {
|
|
602
|
+
if (wss || httpServer) return;
|
|
603
|
+
webToken = crypto.randomBytes(16).toString("hex");
|
|
604
|
+
try {
|
|
605
|
+
httpServer = http.createServer(handleHttp);
|
|
606
|
+
wss = new WebSocketServer({ server: httpServer });
|
|
607
|
+
httpServer.on("error", () => {
|
|
608
|
+
try {
|
|
609
|
+
httpServer?.close();
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
httpServer = null;
|
|
613
|
+
wss = null;
|
|
614
|
+
});
|
|
615
|
+
httpServer.listen(0, "127.0.0.1", () => {
|
|
616
|
+
const addr = httpServer?.address();
|
|
617
|
+
if (addr && typeof addr === "object") {
|
|
618
|
+
port = addr.port;
|
|
619
|
+
writeEntry();
|
|
620
|
+
if (!heartbeat) {
|
|
621
|
+
heartbeat = setInterval(writeEntry, HEARTBEAT_INTERVAL_MS);
|
|
622
|
+
heartbeat.unref?.();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
} catch {
|
|
627
|
+
try {
|
|
628
|
+
httpServer?.close();
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
httpServer = null;
|
|
632
|
+
wss = null;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
wss.on("connection", (ws) => {
|
|
636
|
+
flushPending();
|
|
637
|
+
client = ws;
|
|
638
|
+
epoch++;
|
|
639
|
+
sentCount = 0;
|
|
640
|
+
reqSeq = 0;
|
|
641
|
+
send(ws, { type: "hello", protocolVersion: PROTOCOL_VERSION, sessionId, meta });
|
|
642
|
+
const live = readSessionMessages(latestCtx);
|
|
643
|
+
if (live.length) lastMessages = live;
|
|
644
|
+
const backlog = linearize(lastMessages);
|
|
645
|
+
if (backlog.length) {
|
|
646
|
+
send(ws, { type: "sync", reqId: ++reqSeq, full: true, blocks: backlog, contextWindow });
|
|
647
|
+
sentCount = backlog.length;
|
|
648
|
+
}
|
|
649
|
+
ws.on("message", (data) => {
|
|
650
|
+
if (ws !== client) return;
|
|
651
|
+
let msg;
|
|
652
|
+
try {
|
|
653
|
+
msg = JSON.parse(data.toString());
|
|
654
|
+
} catch {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (msg?.type === "plan" && typeof msg.reqId === "number") {
|
|
658
|
+
const resolve2 = pending.get(msg.reqId);
|
|
659
|
+
if (resolve2) {
|
|
660
|
+
pending.delete(msg.reqId);
|
|
661
|
+
resolve2({ ops: Array.isArray(msg.ops) ? msg.ops : [], groups: Array.isArray(msg.groups) ? msg.groups : [] });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (msg?.type === "unfoldResult" && typeof msg.reqId === "number") {
|
|
665
|
+
const resolve2 = pendingUnfold.get(msg.reqId);
|
|
666
|
+
if (resolve2) {
|
|
667
|
+
pendingUnfold.delete(msg.reqId);
|
|
668
|
+
resolve2({
|
|
669
|
+
restored: Array.isArray(msg.restored) ? msg.restored : [],
|
|
670
|
+
missing: Array.isArray(msg.missing) ? msg.missing : []
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (msg?.type === "recallResult" && typeof msg.reqId === "number") {
|
|
675
|
+
const resolve2 = pendingRecall.get(msg.reqId);
|
|
676
|
+
if (resolve2) {
|
|
677
|
+
pendingRecall.delete(msg.reqId);
|
|
678
|
+
resolve2({
|
|
679
|
+
restored: Array.isArray(msg.restored) ? msg.restored : [],
|
|
680
|
+
missing: Array.isArray(msg.missing) ? msg.missing : []
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (msg?.type === "completeRequest" && typeof msg.reqId === "number") {
|
|
685
|
+
const req = msg;
|
|
686
|
+
const capturedWs = ws;
|
|
687
|
+
void (async () => {
|
|
688
|
+
const reply = (r) => {
|
|
689
|
+
if (capturedWs === client && capturedWs.readyState === 1) send(capturedWs, r);
|
|
690
|
+
};
|
|
691
|
+
if (typeof req.prompt !== "string" || req.prompt.length === 0) {
|
|
692
|
+
reply({ type: "completeResult", reqId: req.reqId, ok: false, error: "missing or empty prompt" });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
const ctx = latestCtx;
|
|
697
|
+
const m = latestModel ?? ctx?.model;
|
|
698
|
+
if (!ctx || !m) {
|
|
699
|
+
reply({ type: "completeResult", reqId: req.reqId, ok: false, error: "no model available" });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(m);
|
|
703
|
+
if (!auth.ok) {
|
|
704
|
+
reply({ type: "completeResult", reqId: req.reqId, ok: false, error: `could not resolve API key: ${auth.error ?? "unknown"}` });
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const { complete } = await import("@earendil-works/pi-ai");
|
|
708
|
+
const context = {
|
|
709
|
+
...typeof req.system === "string" ? { systemPrompt: req.system } : {},
|
|
710
|
+
messages: [{ role: "user", content: req.prompt, timestamp: Date.now() }]
|
|
711
|
+
};
|
|
712
|
+
let maxTokens;
|
|
713
|
+
if (typeof req.maxOutputTokens === "number" && req.maxOutputTokens > 0) {
|
|
714
|
+
const modelCeiling = typeof m.maxTokens === "number" && m.maxTokens > 0 ? m.maxTokens : void 0;
|
|
715
|
+
maxTokens = modelCeiling !== void 0 ? Math.min(req.maxOutputTokens, modelCeiling) : req.maxOutputTokens;
|
|
716
|
+
}
|
|
717
|
+
const result = await complete(m, context, {
|
|
718
|
+
apiKey: auth.apiKey,
|
|
719
|
+
headers: auth.headers,
|
|
720
|
+
...maxTokens !== void 0 ? { maxTokens } : {}
|
|
721
|
+
});
|
|
722
|
+
let text = "";
|
|
723
|
+
if (Array.isArray(result.content)) {
|
|
724
|
+
text = result.content.filter((p) => p?.type === "text").map((p) => typeof p?.text === "string" ? p.text : "").join("");
|
|
725
|
+
}
|
|
726
|
+
reply({
|
|
727
|
+
type: "completeResult",
|
|
728
|
+
reqId: req.reqId,
|
|
729
|
+
ok: true,
|
|
730
|
+
text,
|
|
731
|
+
model: result.model,
|
|
732
|
+
inputTokens: typeof result.usage?.input === "number" ? result.usage.input : void 0,
|
|
733
|
+
outputTokens: typeof result.usage?.output === "number" ? result.usage.output : void 0
|
|
734
|
+
});
|
|
735
|
+
} catch (err) {
|
|
736
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
737
|
+
reply({ type: "completeResult", reqId: req.reqId, ok: false, error: errMsg });
|
|
738
|
+
}
|
|
739
|
+
})();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
const drop = () => {
|
|
743
|
+
if (client === ws) client = null;
|
|
744
|
+
};
|
|
745
|
+
ws.on("close", drop);
|
|
746
|
+
ws.on("error", drop);
|
|
747
|
+
});
|
|
748
|
+
wss.on("error", () => {
|
|
749
|
+
try {
|
|
750
|
+
httpServer?.close();
|
|
751
|
+
} catch {
|
|
752
|
+
}
|
|
753
|
+
httpServer = null;
|
|
754
|
+
wss = null;
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
function requestPlan(reqId, full, blocks) {
|
|
758
|
+
return new Promise((resolve2) => {
|
|
759
|
+
const ws = client;
|
|
760
|
+
if (!ws || ws.readyState !== 1) return resolve2(null);
|
|
761
|
+
const timer = setTimeout(() => {
|
|
762
|
+
if (pending.has(reqId)) {
|
|
763
|
+
pending.delete(reqId);
|
|
764
|
+
resolve2({ ops: [], groups: [] });
|
|
765
|
+
}
|
|
766
|
+
}, REQUEST_TIMEOUT_MS);
|
|
767
|
+
pending.set(reqId, (plan) => {
|
|
768
|
+
clearTimeout(timer);
|
|
769
|
+
resolve2(plan);
|
|
770
|
+
});
|
|
771
|
+
send(ws, { type: "sync", reqId, full, blocks, contextWindow });
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
function requestUnfold(codes) {
|
|
775
|
+
return new Promise((resolve2) => {
|
|
776
|
+
const ws = client;
|
|
777
|
+
if (!ws || ws.readyState !== 1) return resolve2(null);
|
|
778
|
+
const reqId = ++unfoldSeq;
|
|
779
|
+
const timer = setTimeout(() => {
|
|
780
|
+
if (pendingUnfold.has(reqId)) {
|
|
781
|
+
pendingUnfold.delete(reqId);
|
|
782
|
+
resolve2(null);
|
|
783
|
+
}
|
|
784
|
+
}, UNFOLD_TIMEOUT_MS);
|
|
785
|
+
pendingUnfold.set(reqId, (res) => {
|
|
786
|
+
clearTimeout(timer);
|
|
787
|
+
resolve2(res);
|
|
788
|
+
});
|
|
789
|
+
send(ws, { type: "unfoldRequest", reqId, codes });
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
function requestRecall(codes) {
|
|
793
|
+
return new Promise((resolve2) => {
|
|
794
|
+
const ws = client;
|
|
795
|
+
if (!ws || ws.readyState !== 1) return resolve2(null);
|
|
796
|
+
const reqId = ++recallSeq;
|
|
797
|
+
const timer = setTimeout(() => {
|
|
798
|
+
if (pendingRecall.has(reqId)) {
|
|
799
|
+
pendingRecall.delete(reqId);
|
|
800
|
+
resolve2(null);
|
|
801
|
+
}
|
|
802
|
+
}, RECALL_TIMEOUT_MS);
|
|
803
|
+
pendingRecall.set(reqId, (res) => {
|
|
804
|
+
clearTimeout(timer);
|
|
805
|
+
resolve2(res);
|
|
806
|
+
});
|
|
807
|
+
send(ws, { type: "recallRequest", reqId, codes });
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
pi.on("session_start", (_event, ctx) => {
|
|
811
|
+
latestCtx = ctx;
|
|
812
|
+
sessionId = `s-${process.pid}-${Date.now()}`;
|
|
813
|
+
sentCount = 0;
|
|
814
|
+
pendingSince = [];
|
|
815
|
+
lastMessages = readSessionMessages(ctx);
|
|
816
|
+
startedAt = Date.now();
|
|
817
|
+
try {
|
|
818
|
+
meta = { title: "pi session", cwd: process?.cwd?.() ?? "", model: "", contextWindow: null, format: "pi" };
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
refreshFromCtx(ctx);
|
|
822
|
+
startServer();
|
|
823
|
+
try {
|
|
824
|
+
ctx.ui.setStatus("accordion", ctx.ui.theme.fg("accent", "\u{1FA97} accordion"));
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
pi.on("message_update", (event) => {
|
|
829
|
+
const ws = client;
|
|
830
|
+
if (!ws || ws.readyState !== 1) return;
|
|
831
|
+
const ev = event?.assistantMessageEvent;
|
|
832
|
+
if (!ev || typeof ev.type !== "string") return;
|
|
833
|
+
const t = ev.type;
|
|
834
|
+
const ci = typeof ev.contentIndex === "number" ? ev.contentIndex : 0;
|
|
835
|
+
if (t === "text_start") {
|
|
836
|
+
sendStream({ type: "stream", phase: "start", kind: "text", contentIndex: ci });
|
|
837
|
+
} else if (t === "thinking_start") {
|
|
838
|
+
sendStream({ type: "stream", phase: "start", kind: "thinking", contentIndex: ci });
|
|
839
|
+
} else if (t === "toolcall_start") {
|
|
840
|
+
sendStream({ type: "stream", phase: "start", kind: "tool_call", contentIndex: ci });
|
|
841
|
+
} else if (t === "text_end") {
|
|
842
|
+
sendStream({ type: "stream", phase: "end", kind: "text", contentIndex: ci });
|
|
843
|
+
} else if (t === "thinking_end") {
|
|
844
|
+
sendStream({ type: "stream", phase: "end", kind: "thinking", contentIndex: ci });
|
|
845
|
+
} else if (t === "toolcall_end") {
|
|
846
|
+
sendStream({ type: "stream", phase: "end", kind: "tool_call", contentIndex: ci });
|
|
847
|
+
} else if (t === "error" || t === "aborted") {
|
|
848
|
+
sendStream({ type: "stream", phase: "abort", kind: "text", contentIndex: -1 });
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
pi.on("context", async (event, ctx) => {
|
|
852
|
+
latestCtx = ctx;
|
|
853
|
+
const myEpoch = epoch;
|
|
854
|
+
refreshFromCtx(ctx);
|
|
855
|
+
lastMessages = event.messages;
|
|
856
|
+
pendingSince = [];
|
|
857
|
+
const all = linearize(lastMessages);
|
|
858
|
+
if (!attached()) return;
|
|
859
|
+
const fresh = all.slice(sentCount);
|
|
860
|
+
const reqId = ++reqSeq;
|
|
861
|
+
const full = sentCount === 0;
|
|
862
|
+
const plan = await requestPlan(reqId, full, fresh);
|
|
863
|
+
if (plan === null) return;
|
|
864
|
+
if (epoch !== myEpoch) return;
|
|
865
|
+
sentCount = Math.max(sentCount, all.length);
|
|
866
|
+
if (plan.ops.length === 0 && plan.groups.length === 0) return;
|
|
867
|
+
return { messages: applyPlan(event.messages, plan.ops, plan.groups) };
|
|
868
|
+
});
|
|
869
|
+
pi.on("model_select", (event) => {
|
|
870
|
+
applyModel(event?.model);
|
|
871
|
+
const ws = client;
|
|
872
|
+
if (ws && ws.readyState === 1) {
|
|
873
|
+
send(ws, { type: "sync", reqId: ++reqSeq, full: false, blocks: [], contextWindow });
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
pi.on("message_end", (event) => {
|
|
877
|
+
const ws = client;
|
|
878
|
+
if (!ws || ws.readyState !== 1) return;
|
|
879
|
+
sendStream({ type: "stream", phase: "abort", kind: "text", contentIndex: -1 });
|
|
880
|
+
const msg = event.message;
|
|
881
|
+
const msgIds = new Set(linearize([msg]).map((b) => b.id));
|
|
882
|
+
const baseIds = new Set(linearize(lastMessages).map((b) => b.id));
|
|
883
|
+
const pendIds = new Set(linearize(pendingSince).map((b) => b.id));
|
|
884
|
+
const alreadySeen = [...msgIds].some((id) => baseIds.has(id) || pendIds.has(id));
|
|
885
|
+
if (msgIds.size > 0 && !alreadySeen) pendingSince.push(msg);
|
|
886
|
+
const all = linearize([...lastMessages, ...pendingSince]);
|
|
887
|
+
if (all.length <= sentCount) return;
|
|
888
|
+
const reqId = ++reqSeq;
|
|
889
|
+
const full = sentCount === 0;
|
|
890
|
+
send(ws, { type: "sync", reqId, full, blocks: all.slice(sentCount) });
|
|
891
|
+
sentCount = all.length;
|
|
892
|
+
});
|
|
893
|
+
pi.on("agent_end", (event, ctx) => {
|
|
894
|
+
latestCtx = ctx;
|
|
895
|
+
lastMessages = event.messages;
|
|
896
|
+
pendingSince = [];
|
|
897
|
+
const ws = client;
|
|
898
|
+
if (!ws || ws.readyState !== 1) return;
|
|
899
|
+
sendStream({ type: "stream", phase: "abort", kind: "text", contentIndex: -1 });
|
|
900
|
+
const all = linearize(lastMessages);
|
|
901
|
+
if (all.length <= sentCount) return;
|
|
902
|
+
const reqId = ++reqSeq;
|
|
903
|
+
const full = sentCount === 0;
|
|
904
|
+
send(ws, { type: "sync", reqId, full, blocks: all.slice(sentCount) });
|
|
905
|
+
sentCount = all.length;
|
|
906
|
+
});
|
|
907
|
+
pi.on("session_before_compact", (_event, ctx) => {
|
|
908
|
+
if (attached()) {
|
|
909
|
+
try {
|
|
910
|
+
ctx.ui.notify("Accordion attached \u2014 native compaction suppressed.", "info");
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
return { cancel: true };
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
pi.on("session_shutdown", () => {
|
|
917
|
+
if (heartbeat) {
|
|
918
|
+
clearInterval(heartbeat);
|
|
919
|
+
heartbeat = null;
|
|
920
|
+
}
|
|
921
|
+
deleteEntry();
|
|
922
|
+
flushPending();
|
|
923
|
+
try {
|
|
924
|
+
client?.close();
|
|
925
|
+
} catch {
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
wss?.close();
|
|
929
|
+
} catch {
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
httpServer?.close();
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
httpServer = null;
|
|
936
|
+
wss = null;
|
|
937
|
+
client = null;
|
|
938
|
+
latestCtx = null;
|
|
939
|
+
});
|
|
940
|
+
pi.registerCommand("accordion", {
|
|
941
|
+
description: "Open/focus Accordion on this pi session",
|
|
942
|
+
handler: async (_args, ctx) => {
|
|
943
|
+
writeFocusRequest();
|
|
944
|
+
const wasAttached = attached();
|
|
945
|
+
const launch = wasAttached ? null : await launchAccordionApp(pi);
|
|
946
|
+
const action = launchResultLine(launch);
|
|
947
|
+
const lines = [
|
|
948
|
+
action.text,
|
|
949
|
+
`Live link: ${wasAttached ? "attached" : "detached"} \xB7 port ${port || "starting"} \xB7 streamed ${sentCount} blocks`
|
|
950
|
+
];
|
|
951
|
+
if (port && webToken) lines.push(`Browser: http://127.0.0.1:${port}/?token=${webToken}`);
|
|
952
|
+
else lines.push("Browser: starting\u2026");
|
|
953
|
+
ctx.ui.notify(lines.join("\n"), action.type);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
pi.registerTool({
|
|
957
|
+
name: "unfold",
|
|
958
|
+
label: "Unfold Context",
|
|
959
|
+
description: "Restore folded context. Accordion (the live context manager attached to this session) may replace older parts of YOUR OWN context with a short summary tagged like `{#3f9a2c FOLDED}`. The original content is preserved, not lost. Call this tool with the short code(s) from those tags to restore the full content. The restored content reappears in your context on your NEXT turn (your past context changes); this call confirms what was scheduled. Only unfold what you actually need \u2014 it costs tokens.",
|
|
960
|
+
promptSnippet: "unfold(codes) \u2014 restore context folded by Accordion (blocks tagged {#<code> FOLDED}).",
|
|
961
|
+
promptGuidelines: [
|
|
962
|
+
"When you see a `{#<code> FOLDED}` marker in your context (e.g. `{#3f9a2c FOLDED}`), that block was compacted by Accordion to save tokens \u2014 the full content is preserved, not lost. If the summary is not enough for your current task, call `unfold` with the code(s) from the marker(s) to restore them; the content returns on your next turn."
|
|
963
|
+
],
|
|
964
|
+
parameters: Type.Object({
|
|
965
|
+
codes: Type.Array(Type.String({ description: 'A fold code copied verbatim from a {#<code> FOLDED} tag, e.g. "3f9a2c". Always a string (codes may have leading zeros).' }), {
|
|
966
|
+
description: "One or more fold codes to restore to full content."
|
|
967
|
+
})
|
|
968
|
+
}),
|
|
969
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
970
|
+
const codes = Array.isArray(params.codes) ? params.codes.map((s) => String(s).trim()).filter((s) => s.length > 0) : [];
|
|
971
|
+
if (!codes.length) {
|
|
972
|
+
return { content: [{ type: "text", text: 'No fold codes given. Pass the code(s) from a {#<code> FOLDED} tag, e.g. unfold({codes:["3f9a2c"]}).' }] };
|
|
973
|
+
}
|
|
974
|
+
if (!attached()) {
|
|
975
|
+
return { content: [{ type: "text", text: "Accordion isn't attached, so nothing in your context is folded right now \u2014 it is already full." }] };
|
|
976
|
+
}
|
|
977
|
+
const res = await requestUnfold(codes);
|
|
978
|
+
if (res === null) {
|
|
979
|
+
return { content: [{ type: "text", text: "Accordion did not respond. Folded content restores automatically if it detaches; otherwise try again." }], isError: true };
|
|
980
|
+
}
|
|
981
|
+
const lines = [];
|
|
982
|
+
if (res.restored.length) {
|
|
983
|
+
lines.push(`Unfolded ${res.restored.length} block(s); full content returns on your next turn:`);
|
|
984
|
+
for (const r of res.restored) lines.push(` \u2022 ${r?.label ?? "block"} (#${r?.code ?? "?"})`);
|
|
985
|
+
}
|
|
986
|
+
if (res.missing.length) {
|
|
987
|
+
lines.push(`No folded block for: ${res.missing.map((c) => "#" + c).join(", ")} (already full, or not in this session's context).`);
|
|
988
|
+
}
|
|
989
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: res };
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
pi.registerTool({
|
|
993
|
+
name: "recall",
|
|
994
|
+
label: "Recall Folded Content",
|
|
995
|
+
description: "Read folded context WITHOUT changing what's standing in your context. Accordion (the live context manager attached to this session) may replace older parts of YOUR OWN context with a short summary tagged like `{#3f9a2c FOLDED}`. The original content is preserved, not lost. Call this tool with the short code(s) from those tags to get the FULL original content back AS THIS tool's result, immediately \u2014 like reading a file. Unlike `unfold`, recall does NOT force the block open: your standing context is unchanged (the block stays folded), so recall costs nothing beyond this one tool result. Use it when you need folded detail RIGHT NOW for the current step.",
|
|
996
|
+
promptSnippet: "recall(codes) \u2014 read folded content right now (returned as the tool result; does not change your standing context).",
|
|
997
|
+
promptGuidelines: [
|
|
998
|
+
"When you see a `{#<code> FOLDED}` marker and need the full content for the current step, call `recall` with the code(s) \u2014 the full original content comes back as this tool's result immediately, and your standing context is left unchanged (the block stays folded). Prefer `recall` over `unfold` when you only need the detail once; use `unfold` when you want the block to stay open across future turns."
|
|
999
|
+
],
|
|
1000
|
+
parameters: Type.Object({
|
|
1001
|
+
codes: Type.Array(Type.String({ description: 'A fold code copied verbatim from a {#<code> FOLDED} tag, e.g. "3f9a2c". Always a string (codes may have leading zeros).' }), {
|
|
1002
|
+
description: "One or more fold codes whose full original content to read."
|
|
1003
|
+
})
|
|
1004
|
+
}),
|
|
1005
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1006
|
+
const codes = Array.isArray(params.codes) ? params.codes.map((s) => String(s).trim()).filter((s) => s.length > 0) : [];
|
|
1007
|
+
if (!codes.length) {
|
|
1008
|
+
return { content: [{ type: "text", text: 'No fold codes given. Pass the code(s) from a {#<code> FOLDED} tag, e.g. recall({codes:["3f9a2c"]}).' }] };
|
|
1009
|
+
}
|
|
1010
|
+
if (!attached()) {
|
|
1011
|
+
return { content: [{ type: "text", text: "Accordion isn't attached, so nothing in your context is folded right now \u2014 it is already full." }] };
|
|
1012
|
+
}
|
|
1013
|
+
const res = await requestRecall(codes);
|
|
1014
|
+
if (res === null) {
|
|
1015
|
+
return { content: [{ type: "text", text: "Accordion did not respond. If it has detached, your context is already full; otherwise try again." }], isError: true };
|
|
1016
|
+
}
|
|
1017
|
+
const content = [];
|
|
1018
|
+
for (const r of res.restored) {
|
|
1019
|
+
content.push({ type: "text", text: `[recalled ${r?.label ?? "block"} (#${r?.code ?? "?"})]
|
|
1020
|
+
${r?.text ?? ""}` });
|
|
1021
|
+
}
|
|
1022
|
+
if (res.missing.length) {
|
|
1023
|
+
content.push({ type: "text", text: `No folded block for: ${res.missing.map((c) => "#" + c).join(", ")} (already full, or not in this session's context).` });
|
|
1024
|
+
}
|
|
1025
|
+
if (!content.length) {
|
|
1026
|
+
content.push({ type: "text", text: "Nothing to recall." });
|
|
1027
|
+
}
|
|
1028
|
+
return { content, details: res };
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
pi.on("resources_discover", () => {
|
|
1032
|
+
try {
|
|
1033
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1034
|
+
const skillPaths = [];
|
|
1035
|
+
for (const name of ["accordion-context-folding", "accordion-context-recall"]) {
|
|
1036
|
+
const dir = path.join(here, "skills", name);
|
|
1037
|
+
if (fs.existsSync(dir)) skillPaths.push(dir);
|
|
1038
|
+
}
|
|
1039
|
+
if (skillPaths.length) return { skillPaths };
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
return {};
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
var BROWSER_FALLBACK_PORT = DEFAULT_PORT;
|
|
1046
|
+
export {
|
|
1047
|
+
BROWSER_FALLBACK_PORT,
|
|
1048
|
+
accordionLive as default
|
|
1049
|
+
};
|