@turri-dev/cli 0.1.16-next.ce2cd46 → 0.1.17-next.1ecfd4f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-commands.d.ts +25 -0
- package/dist/chat-commands.js +162 -0
- package/dist/chat-records.d.ts +44 -0
- package/dist/chat-records.js +102 -0
- package/dist/chat-scope.d.ts +14 -0
- package/dist/chat-scope.js +15 -0
- package/dist/chat-terminal.d.ts +5 -0
- package/dist/chat-terminal.js +136 -0
- package/dist/chat-ui.d.ts +33 -0
- package/dist/chat-ui.js +192 -0
- package/dist/chats.d.ts +176 -0
- package/dist/chats.js +356 -0
- package/dist/cli.js +5 -7
- package/dist/config.d.ts +8 -2
- package/dist/config.js +18 -4
- package/dist/extension.d.ts +20 -33
- package/dist/extension.js +348 -230
- package/dist/host-session.d.ts +51 -0
- package/dist/host-session.js +58 -0
- package/dist/host.d.ts +5 -4
- package/dist/host.js +6 -5
- package/dist/identity.js +5 -0
- package/dist/pi-launch.js +23 -1
- package/dist/regenerate/jobs.d.ts +2 -0
- package/dist/regenerate/jobs.js +3 -0
- package/dist/regenerate/ledger.d.ts +7 -0
- package/dist/regenerate/ledger.js +8 -1
- package/dist/regenerate/test-scripts.d.ts +1 -1
- package/dist/stacks.d.ts +2 -1
- package/dist/stacks.js +4 -3
- package/docs/README.md +4 -2
- package/docs/chats.md +73 -0
- package/docs/config.md +3 -3
- package/docs/development.md +4 -1
- package/docs/hosting.md +35 -28
- package/docs/using.md +2 -2
- package/package.json +1 -1
package/dist/chats.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { currentChat, inChat } from "./chat-scope.js";
|
|
4
|
+
import { adoptRun, forgetChat, recordRun } from "./chat-records.js";
|
|
5
|
+
import { createChatUi } from "./chat-ui.js";
|
|
6
|
+
import { debugMode } from "./config.js";
|
|
7
|
+
import { record } from "./regenerate/ledger.js";
|
|
8
|
+
/**
|
|
9
|
+
* The chats of a turri session.
|
|
10
|
+
*
|
|
11
|
+
* One turri process is one session on one project, and holds up to MAX_CHATS chats: conversations, each with its own
|
|
12
|
+
* history, model, thinking level, stack and questions, each working while another is shown. What they share is the
|
|
13
|
+
* session's: the one job registry and work loop the project's pulses run in - a pulse any chat starts is in every
|
|
14
|
+
* chat's side panel - the coding model, steering, releases and the ledger.
|
|
15
|
+
*
|
|
16
|
+
* A chat is a pi session with turri's extension in it. The extension, built once for each, links itself to its chat
|
|
17
|
+
* here (`attach`); its tools, commands and handlers then run inside the chat's scope (chat-scope.ts), so a pulse knows
|
|
18
|
+
* which chat asked for it, and is reported back there when it ends - to the chat shown, when that one is closed.
|
|
19
|
+
*
|
|
20
|
+
* Where the chats live is a driver's: the terminal's (chat-terminal.ts) swaps which one pi's screen shows; a host's
|
|
21
|
+
* (`createTurri`) makes a session for each and shows them in its own windows.
|
|
22
|
+
*/
|
|
23
|
+
/** The most chats a session has open at once. */
|
|
24
|
+
export const MAX_CHATS = 9;
|
|
25
|
+
/** What a chat is called until something is said in it. */
|
|
26
|
+
export const NEW_CHAT_TITLE = "new chat";
|
|
27
|
+
/** A chat's title from what it says: its name, else its first message, short. */
|
|
28
|
+
function titleOf(session) {
|
|
29
|
+
if (session === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
try {
|
|
32
|
+
const named = session.sessionManager.getSessionName()?.trim();
|
|
33
|
+
if (named)
|
|
34
|
+
return named;
|
|
35
|
+
for (const message of session.messages) {
|
|
36
|
+
if (message.role !== "user")
|
|
37
|
+
continue;
|
|
38
|
+
const content = message.content;
|
|
39
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => part.text ?? "").join(" ") : "";
|
|
40
|
+
const words = text.replace(/\s+/g, " ").trim();
|
|
41
|
+
if (words !== "")
|
|
42
|
+
return words.length > TITLE_CHARS ? `${words.slice(0, TITLE_CHARS - 1)}…` : words;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* a session let go has no title to read */
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const TITLE_CHARS = 32;
|
|
51
|
+
export function createChats(options) {
|
|
52
|
+
const { cwd, host, jobs } = options;
|
|
53
|
+
const chats = [];
|
|
54
|
+
const listeners = new Set();
|
|
55
|
+
let driver;
|
|
56
|
+
let shownId;
|
|
57
|
+
/** The chat whose extension pi is building now. */
|
|
58
|
+
let attaching;
|
|
59
|
+
/** Chats are opened one at a time: each one's extension is built while it is the one attaching. */
|
|
60
|
+
let opening = Promise.resolve();
|
|
61
|
+
let notices = [];
|
|
62
|
+
const run = { run: randomUUID().slice(0, 8), pid: process.pid, started: new Date().toISOString(), chats: [] };
|
|
63
|
+
const changed = () => {
|
|
64
|
+
for (const listener of listeners) {
|
|
65
|
+
try {
|
|
66
|
+
listener();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* a listener that fails is left out of this change */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/** A chat nothing was said in yet is a new chat: its number is beside it already, in the strip and the picker. */
|
|
74
|
+
const titleFor = (chat) => titleOf(chat.session) ?? chat.recordedTitle ?? NEW_CHAT_TITLE;
|
|
75
|
+
const statusOf = (chat) => (chat.ui?.waiting() ?? 0) > 0 ? "question" : chat.working ? "working" : chat.unread ? "unread" : "idle";
|
|
76
|
+
/** What the session keeps of its chats on disk: those with something said in them, in order. */
|
|
77
|
+
const persist = () => {
|
|
78
|
+
if (options.persist !== true)
|
|
79
|
+
return;
|
|
80
|
+
run.chats = chats.flatMap((chat) => {
|
|
81
|
+
const file = chat.session?.sessionFile;
|
|
82
|
+
return file !== undefined && existsSync(file) ? [{ id: chat.id, title: titleFor(chat), sessionFile: file }] : [];
|
|
83
|
+
});
|
|
84
|
+
recordRun(cwd, run, { keep: debugMode(cwd) });
|
|
85
|
+
};
|
|
86
|
+
const newChat = (title) => {
|
|
87
|
+
const id = randomUUID().slice(0, 8);
|
|
88
|
+
const chat = {
|
|
89
|
+
id,
|
|
90
|
+
scope: { id, stack: process.env.TURRI_STACK?.trim() || undefined },
|
|
91
|
+
...(title !== undefined ? { recordedTitle: title } : {}),
|
|
92
|
+
working: false,
|
|
93
|
+
unread: false,
|
|
94
|
+
};
|
|
95
|
+
chat.dialogs = options.dialogs?.(id);
|
|
96
|
+
chats.push(chat);
|
|
97
|
+
return chat;
|
|
98
|
+
};
|
|
99
|
+
/** What the chat's session does, as the session follows it: its turns, its name. */
|
|
100
|
+
const follow = (chat) => {
|
|
101
|
+
chat.unsubscribe?.();
|
|
102
|
+
const session = chat.session;
|
|
103
|
+
if (session === undefined)
|
|
104
|
+
return;
|
|
105
|
+
chat.unsubscribe = session.subscribe((event) => {
|
|
106
|
+
if (event.type === "agent_start") {
|
|
107
|
+
chat.working = true;
|
|
108
|
+
changed();
|
|
109
|
+
}
|
|
110
|
+
else if (event.type === "agent_end") {
|
|
111
|
+
chat.working = false;
|
|
112
|
+
if (shownId !== chat.id)
|
|
113
|
+
chat.unread = true;
|
|
114
|
+
persist();
|
|
115
|
+
changed();
|
|
116
|
+
}
|
|
117
|
+
else if (event.type === "session_info_changed") {
|
|
118
|
+
persist();
|
|
119
|
+
changed();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const core = {
|
|
124
|
+
cwd,
|
|
125
|
+
host,
|
|
126
|
+
jobs,
|
|
127
|
+
extension: (pi) => options.extension(pi, core.attach(pi)),
|
|
128
|
+
get canOpen() {
|
|
129
|
+
return driver !== undefined;
|
|
130
|
+
},
|
|
131
|
+
get terminal() {
|
|
132
|
+
return driver?.terminal === true;
|
|
133
|
+
},
|
|
134
|
+
chats: () => chats.map((chat) => ({ id: chat.id, title: titleFor(chat), status: statusOf(chat), shown: chat.id === shownId })),
|
|
135
|
+
session: (id) => chats.find((chat) => chat.id === id)?.session,
|
|
136
|
+
shown: () => shownId,
|
|
137
|
+
shownChat: () => chats.find((chat) => chat.id === shownId),
|
|
138
|
+
chatOfSession: (session) => chats.find((chat) => chat.session === session),
|
|
139
|
+
async open(asked = {}) {
|
|
140
|
+
if (driver === undefined)
|
|
141
|
+
throw new Error("This turri has one chat: there is nowhere to open another.");
|
|
142
|
+
const done = opening.then(async () => {
|
|
143
|
+
const already = asked.sessionFile !== undefined ? chats.find((chat) => chat.session?.sessionFile === asked.sessionFile) : undefined;
|
|
144
|
+
if (already !== undefined)
|
|
145
|
+
return already;
|
|
146
|
+
if (chats.length >= MAX_CHATS)
|
|
147
|
+
throw new Error(`${MAX_CHATS} chats are open, the most there can be: close one first.`);
|
|
148
|
+
const chat = newChat(asked.title);
|
|
149
|
+
attaching = chat;
|
|
150
|
+
try {
|
|
151
|
+
await driver.create(chat, asked.sessionFile);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
chats.splice(chats.indexOf(chat), 1);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
attaching = undefined;
|
|
159
|
+
}
|
|
160
|
+
follow(chat);
|
|
161
|
+
inChat(chat.scope, () => record({ kind: "chat.opened", reopened: asked.sessionFile !== undefined }, { project: cwd }));
|
|
162
|
+
persist();
|
|
163
|
+
changed();
|
|
164
|
+
return chat;
|
|
165
|
+
});
|
|
166
|
+
opening = done.catch(() => undefined);
|
|
167
|
+
const chat = await done;
|
|
168
|
+
if (asked.hidden !== true)
|
|
169
|
+
await core.show(chat.id);
|
|
170
|
+
return core.chats().find((info) => info.id === chat.id);
|
|
171
|
+
},
|
|
172
|
+
async close(id) {
|
|
173
|
+
const index = chats.findIndex((chat) => chat.id === id);
|
|
174
|
+
if (index === -1)
|
|
175
|
+
return;
|
|
176
|
+
const chat = chats[index];
|
|
177
|
+
if (shownId === id) {
|
|
178
|
+
const next = chats[index + 1] ?? chats[index - 1];
|
|
179
|
+
if (next !== undefined)
|
|
180
|
+
await core.show(next.id);
|
|
181
|
+
else
|
|
182
|
+
shownId = undefined;
|
|
183
|
+
}
|
|
184
|
+
const recorded = run.chats.find((kept) => kept.id === id);
|
|
185
|
+
chats.splice(chats.indexOf(chat), 1);
|
|
186
|
+
chat.unsubscribe?.();
|
|
187
|
+
chat.ui?.close();
|
|
188
|
+
inChat(chat.scope, () => record({ kind: "chat.closed" }, { project: cwd }));
|
|
189
|
+
try {
|
|
190
|
+
await driver?.dispose(chat);
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
if (options.persist === true) {
|
|
194
|
+
forgetChat(cwd, run.run, recorded, { keep: debugMode(cwd) });
|
|
195
|
+
run.chats = run.chats.filter((kept) => kept.id !== id);
|
|
196
|
+
}
|
|
197
|
+
changed();
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
async show(id) {
|
|
201
|
+
const chat = chats.find((candidate) => candidate.id === id);
|
|
202
|
+
if (chat === undefined || shownId === id)
|
|
203
|
+
return;
|
|
204
|
+
const from = core.shownChat();
|
|
205
|
+
if (driver?.show !== undefined)
|
|
206
|
+
await driver.show(chat, from);
|
|
207
|
+
else
|
|
208
|
+
core.showing(chat);
|
|
209
|
+
},
|
|
210
|
+
showing(chat) {
|
|
211
|
+
shownId = chat.id;
|
|
212
|
+
chat.unread = false;
|
|
213
|
+
for (const [message, level] of notices.splice(0))
|
|
214
|
+
core.notify(message, level);
|
|
215
|
+
changed();
|
|
216
|
+
},
|
|
217
|
+
onChats(listener) {
|
|
218
|
+
listeners.add(listener);
|
|
219
|
+
return () => listeners.delete(listener);
|
|
220
|
+
},
|
|
221
|
+
changed,
|
|
222
|
+
notify(message, level) {
|
|
223
|
+
const chat = core.shownChat();
|
|
224
|
+
const ui = chat?.ui?.ui ?? chat?.session?.extensionRunner.getUIContext();
|
|
225
|
+
if (ui === undefined) {
|
|
226
|
+
notices = [...notices, [message, level]].slice(-10);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
ui.notify(message, level);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
/* a chat let go says nothing */
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
async dispose(asked = {}) {
|
|
237
|
+
for (const chat of chats.splice(0)) {
|
|
238
|
+
chat.unsubscribe?.();
|
|
239
|
+
chat.ui?.close();
|
|
240
|
+
if (chat.session === asked.except)
|
|
241
|
+
continue;
|
|
242
|
+
try {
|
|
243
|
+
await driver?.dispose(chat);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
/* let go already */
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
changed();
|
|
250
|
+
},
|
|
251
|
+
attach() {
|
|
252
|
+
// The chat pi builds this extension for: the one being opened; else, the session's first - pi makes it itself -
|
|
253
|
+
// or, once there is one, the chat shown, rebuilt by pi (a /reload, a /fork).
|
|
254
|
+
const chat = attaching ?? core.shownChat() ?? (chats.length === 0 ? newChat() : chats[chats.length - 1]);
|
|
255
|
+
return {
|
|
256
|
+
id: chat.id,
|
|
257
|
+
scope: chat.scope,
|
|
258
|
+
dialogs: chat.dialogs,
|
|
259
|
+
live: (instance) => {
|
|
260
|
+
chat.instance = instance;
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
adopt(session, services) {
|
|
265
|
+
const chat = core.chatOfSession(session) ?? core.shownChat() ?? chats.find((candidate) => candidate.session === undefined) ?? newChat();
|
|
266
|
+
if (chat.session !== session) {
|
|
267
|
+
chat.session = session;
|
|
268
|
+
chat.services = services;
|
|
269
|
+
follow(chat);
|
|
270
|
+
if (shownId === undefined)
|
|
271
|
+
inChat(chat.scope, () => record({ kind: "chat.opened", reopened: session.messages.length > 0 }, { project: cwd }));
|
|
272
|
+
}
|
|
273
|
+
return chat;
|
|
274
|
+
},
|
|
275
|
+
jobFinished(job) {
|
|
276
|
+
const asked = chats.find((chat) => chat.id === job.chat);
|
|
277
|
+
const chat = asked?.instance !== undefined ? asked : (core.shownChat() ?? chats[0]);
|
|
278
|
+
if (chat === undefined)
|
|
279
|
+
return;
|
|
280
|
+
if (chat.id !== shownId)
|
|
281
|
+
chat.unread = true;
|
|
282
|
+
try {
|
|
283
|
+
chat.instance?.announce(job);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// A chat let go as the pulse ended: the one shown is told instead.
|
|
287
|
+
const shown = core.shownChat();
|
|
288
|
+
if (shown !== undefined && shown !== chat)
|
|
289
|
+
shown.instance?.announce(job);
|
|
290
|
+
}
|
|
291
|
+
changed();
|
|
292
|
+
},
|
|
293
|
+
jobsChanged() {
|
|
294
|
+
for (const chat of chats) {
|
|
295
|
+
try {
|
|
296
|
+
chat.instance?.jobsChanged();
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* a chat let go draws nothing */
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
instanceInScope() {
|
|
304
|
+
const scoped = currentChat();
|
|
305
|
+
return (chats.find((chat) => chat.id === scoped?.id) ?? core.shownChat() ?? chats[0])?.instance;
|
|
306
|
+
},
|
|
307
|
+
setDriver(given) {
|
|
308
|
+
driver = given;
|
|
309
|
+
},
|
|
310
|
+
async restore(saved, asked = {}) {
|
|
311
|
+
adoptRun(cwd, saved.run);
|
|
312
|
+
const [first, ...rest] = saved.chats;
|
|
313
|
+
if (first !== undefined && asked.skipFirst === true) {
|
|
314
|
+
const chat = chats[0];
|
|
315
|
+
if (chat !== undefined)
|
|
316
|
+
chat.recordedTitle ??= first.title;
|
|
317
|
+
}
|
|
318
|
+
for (const kept of asked.skipFirst === true ? rest : saved.chats) {
|
|
319
|
+
try {
|
|
320
|
+
await core.open({ sessionFile: kept.sessionFile, hidden: true, title: kept.title });
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
core.notify(`The chat "${kept.title}" could not be reopened: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
persist();
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
return core;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* pi's extension API for a chat: every tool, command, shortcut and handler the extension registers runs inside the
|
|
333
|
+
* chat's scope - its stack, and its id on what it starts.
|
|
334
|
+
*/
|
|
335
|
+
export function scopedToChat(pi, scope) {
|
|
336
|
+
const within = (work) => work === undefined ? undefined : (...args) => inChat(scope, () => work(...args));
|
|
337
|
+
return new Proxy(pi, {
|
|
338
|
+
get(target, key, receiver) {
|
|
339
|
+
if (key === "registerTool")
|
|
340
|
+
return (tool) => target.registerTool({ ...tool, execute: within(tool.execute) });
|
|
341
|
+
if (key === "registerCommand")
|
|
342
|
+
return (name, command) => target.registerCommand(name, { ...command, handler: within(command.handler) });
|
|
343
|
+
if (key === "registerShortcut")
|
|
344
|
+
return (shortcut, options) => target.registerShortcut(shortcut, { ...options, handler: within(options.handler) });
|
|
345
|
+
if (key === "on")
|
|
346
|
+
return (event, handler) => target.on(event, within(handler));
|
|
347
|
+
const value = Reflect.get(target, key, receiver);
|
|
348
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/** A chat's stand-in for the screen, for the terminal's driver. */
|
|
353
|
+
export function terminalUi(chat, screen, onChange) {
|
|
354
|
+
chat.ui ??= createChatUi({ screen, onChange });
|
|
355
|
+
return chat.ui;
|
|
356
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -52,11 +52,9 @@ function chat(args) {
|
|
|
52
52
|
}
|
|
53
53
|
// pi itself, through a launcher that leaves out pi's startup header - see pi-launch.ts.
|
|
54
54
|
const launcher = join(HERE, `pi-launch${OWN_EXTENSION}`);
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
const
|
|
58
|
-
? []
|
|
59
|
-
: ["-e", join(HERE, `extension${OWN_EXTENSION}`)];
|
|
55
|
+
// turri is built into pi by the launcher - unless an older turri's file still loads it into every pi, which builds it
|
|
56
|
+
// in as well: pi-launch.ts is told, and leaves it to that. turri doctor offers to remove the file.
|
|
57
|
+
const inPi = extensionInstalledGlobally();
|
|
60
58
|
const themes = themesInstalledGlobally() ? [] : ["--theme", join(PACKAGE_ROOT, ".pi", "themes")];
|
|
61
59
|
// turri's own theme, unless this run names another. For this run only: pi's settings
|
|
62
60
|
// are left as they are, so a plain `pi` keeps its theme.
|
|
@@ -66,10 +64,10 @@ function chat(args) {
|
|
|
66
64
|
// Fullscreen: pi owns the viewport, so the conversation stays where it is scrolled to while the
|
|
67
65
|
// jobs redraw - in the terminal's own scrollback, every redraw pulls it back to the bottom.
|
|
68
66
|
const tuiMode = args.some((arg) => arg === "--tui-mode" || arg.startsWith("--tui-mode=")) ? [] : ["--tui-mode", "fullscreen"];
|
|
69
|
-
const result = spawnSync(process.execPath, [launcher, ...
|
|
67
|
+
const result = spawnSync(process.execPath, [launcher, ...themes, ...useTheme, ...tuiMode, ...piArgs], {
|
|
70
68
|
stdio: "inherit",
|
|
71
69
|
cwd: process.cwd(),
|
|
72
|
-
env:
|
|
70
|
+
env: { ...process.env, ...(codingModel ? { [CODING_MODEL_ENV]: codingModel } : {}), ...(inPi ? { TURRI_EXTENSION_IN_PI: "1" } : {}) },
|
|
73
71
|
});
|
|
74
72
|
process.exit(result.status ?? 1);
|
|
75
73
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -85,9 +85,15 @@ export declare function stacksOf(root: string): string[];
|
|
|
85
85
|
export declare function adoptLegacyConfig(root: string, options?: {
|
|
86
86
|
projectFile?: boolean;
|
|
87
87
|
}): string[];
|
|
88
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* The stack chosen now - the chat's own (`/stack` in it), else `--stack` or `TURRI_STACK` - or, inside a pulse, the one
|
|
90
|
+
* it started with.
|
|
91
|
+
*/
|
|
89
92
|
export declare function chosenStack(): string | undefined;
|
|
90
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Choose the stack everything is built with from now on - or none, the project's config alone. In a chat, for that
|
|
95
|
+
* chat alone; outside one, kept in TURRI_STACK.
|
|
96
|
+
*/
|
|
91
97
|
export declare function chooseStack(stack: string | undefined): void;
|
|
92
98
|
/** Run `work` with `stack` as the one chosen, whatever is chosen meanwhile: a pulse keeps the stack it started with. */
|
|
93
99
|
export declare function withStack<T>(stack: string | undefined, work: () => T): T;
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { copyFileSync, existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { basename, join, resolve } from "node:path";
|
|
4
4
|
import { load as loadYaml } from "js-yaml";
|
|
5
|
+
import { currentChat } from "./chat-scope.js";
|
|
5
6
|
import { projectDirOf } from "./regenerate/paths.js";
|
|
6
7
|
/**
|
|
7
8
|
* The project's `config.turri.yaml`, at its root - the folder that holds `plain_modules` - or, before it had one, its `config.yaml`.
|
|
@@ -139,19 +140,32 @@ export function adoptLegacyConfig(root, options = {}) {
|
|
|
139
140
|
}
|
|
140
141
|
/** The stack a pulse runs with: the one chosen as it started, whatever is chosen since. */
|
|
141
142
|
const pinnedStack = new AsyncLocalStorage();
|
|
142
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* The stack chosen now - the chat's own (`/stack` in it), else `--stack` or `TURRI_STACK` - or, inside a pulse, the one
|
|
145
|
+
* it started with.
|
|
146
|
+
*/
|
|
143
147
|
export function chosenStack() {
|
|
144
148
|
const pinned = pinnedStack.getStore();
|
|
145
149
|
if (pinned !== undefined)
|
|
146
150
|
return pinned.stack;
|
|
151
|
+
const chat = currentChat();
|
|
152
|
+
if (chat !== undefined)
|
|
153
|
+
return chat.stack;
|
|
147
154
|
return text(process.env.TURRI_STACK);
|
|
148
155
|
}
|
|
149
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Choose the stack everything is built with from now on - or none, the project's config alone. In a chat, for that
|
|
158
|
+
* chat alone; outside one, kept in TURRI_STACK.
|
|
159
|
+
*/
|
|
150
160
|
export function chooseStack(stack) {
|
|
151
|
-
|
|
161
|
+
const chosen = stack === undefined || stack.trim() === "" ? undefined : stack.trim();
|
|
162
|
+
const chat = currentChat();
|
|
163
|
+
if (chat !== undefined)
|
|
164
|
+
chat.stack = chosen;
|
|
165
|
+
else if (chosen === undefined)
|
|
152
166
|
delete process.env.TURRI_STACK;
|
|
153
167
|
else
|
|
154
|
-
process.env.TURRI_STACK =
|
|
168
|
+
process.env.TURRI_STACK = chosen;
|
|
155
169
|
}
|
|
156
170
|
/** Run `work` with `stack` as the one chosen, whatever is chosen meanwhile: a pulse keeps the stack it started with. */
|
|
157
171
|
export function withStack(stack, work) {
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import "./terminal-images.ts";
|
|
2
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { stackOnMachine } from "./stack.ts";
|
|
4
4
|
import { createAskUserTool } from "./tools.ts";
|
|
5
5
|
import { fromWorkingDir } from "./loop-tools.ts";
|
|
6
6
|
import { type WorkLoop } from "./regenerate/loop.ts";
|
|
7
7
|
import { type AskUser, type AskUserIO } from "./ask.ts";
|
|
8
|
-
import { type
|
|
8
|
+
import { type TurriCore, type TurriHost } from "./chats.ts";
|
|
9
9
|
/**
|
|
10
10
|
* Puts a skill step's question to the person, when there is one to ask.
|
|
11
11
|
*
|
|
@@ -29,38 +29,25 @@ dialogs?: AskUserIO): AskUser;
|
|
|
29
29
|
* to start at once.
|
|
30
30
|
*/
|
|
31
31
|
export declare function chatAskUserTool(dialogs?: AskUserIO, afterAnswer?: (answer: string) => string | undefined): ReturnType<typeof createAskUserTool>;
|
|
32
|
+
export type { TurriHost } from "./chats.ts";
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*/
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* where questions appear. Given, turri tells the model none of the terminal's - its keys, the side panel, regen,
|
|
51
|
-
* /pulses - and gives it this instead, as a `<surface>` block in turri's identity. Absent, the terminal is the surface.
|
|
52
|
-
*/
|
|
53
|
-
surface?: string;
|
|
54
|
-
/**
|
|
55
|
-
* The last word on the whole system prompt, as turri has built it for this turn - after its own before_agent_start
|
|
56
|
-
* work: what it returns is what is sent. For a host that needs to change anything else.
|
|
57
|
-
*/
|
|
58
|
-
systemPrompt?(prompt: string): string;
|
|
59
|
-
}
|
|
60
|
-
/** turri's conversation as an extension of pi's, for a host that runs it itself: see TurriHost. */
|
|
61
|
-
export declare function createTurriExtension(host?: TurriHost): (pi: ExtensionAPI) => void;
|
|
62
|
-
declare const _default: (pi: ExtensionAPI) => void;
|
|
63
|
-
export default _default;
|
|
34
|
+
* A turri session: its job registry and work loop, the ledger, and its chats, each with turri's extension in it
|
|
35
|
+
* (chats.ts). The terminal makes one as it starts (pi-launch.ts), a host with `createTurri` (host.ts); `persist`
|
|
36
|
+
* records the open chats for `turri --continue`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createTurriCore(host?: TurriHost, options?: {
|
|
39
|
+
persist?: boolean;
|
|
40
|
+
dialogs?: (chat: string) => AskUserIO | undefined;
|
|
41
|
+
}): TurriCore;
|
|
42
|
+
/** The session of this process: pi-launch.ts's, or one made as pi first builds the extension (`pi -e src/extension.ts`). */
|
|
43
|
+
export declare function processSession(make?: () => TurriCore): TurriCore;
|
|
44
|
+
declare const extension: ExtensionFactory;
|
|
45
|
+
export default extension;
|
|
46
|
+
/**
|
|
47
|
+
* The session's work loop, for a host to read: made with the first chat's context - the models it plans with - when no
|
|
48
|
+
* chat has asked for work yet.
|
|
49
|
+
*/
|
|
50
|
+
export declare function sessionLoop(session: TurriCore): WorkLoop;
|
|
64
51
|
/**
|
|
65
52
|
* What a slash command showed the person: kept in the chat, where they read it, and put in the conversation as well -
|
|
66
53
|
* unseen there - so that when they speak of it next ("fix it"), the agent knows what they saw. A command runs outside
|