picocode-core 0.9.119
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 +15 -0
- package/package.json +33 -0
- package/src/agent-transcript.js +80 -0
- package/src/agent.js +170 -0
- package/src/agents.js +213 -0
- package/src/attachments.js +141 -0
- package/src/boot.js +28 -0
- package/src/catalog-snapshot.json +1 -0
- package/src/catalog.js +86 -0
- package/src/codex-models.js +56 -0
- package/src/commands.js +61 -0
- package/src/compaction.js +82 -0
- package/src/completion.js +21 -0
- package/src/config.js +32 -0
- package/src/context.js +82 -0
- package/src/controller.js +1263 -0
- package/src/conversation-search.js +101 -0
- package/src/deliberation-history.js +65 -0
- package/src/deliberation.js +61 -0
- package/src/derive.js +307 -0
- package/src/events.js +54 -0
- package/src/export.js +16 -0
- package/src/files.js +39 -0
- package/src/format.js +6 -0
- package/src/fuzzy.js +41 -0
- package/src/git.js +156 -0
- package/src/history.js +51 -0
- package/src/init.js +25 -0
- package/src/keys.js +26 -0
- package/src/mcp.js +280 -0
- package/src/memory.js +120 -0
- package/src/models.js +14 -0
- package/src/openai-auth.js +204 -0
- package/src/paths.js +67 -0
- package/src/reversible-edit.js +79 -0
- package/src/rewind.js +84 -0
- package/src/session-index.js +264 -0
- package/src/session-lock.js +27 -0
- package/src/session.js +160 -0
- package/src/shells.js +166 -0
- package/src/skills.js +164 -0
- package/src/steer.js +129 -0
- package/src/system-prompt.js +49 -0
- package/src/terminal-theme.js +49 -0
- package/src/tools/bash.js +184 -0
- package/src/tools/diff.js +18 -0
- package/src/tools/edit.js +95 -0
- package/src/tools/glob.js +43 -0
- package/src/tools/grep.js +59 -0
- package/src/tools/index.js +296 -0
- package/src/tools/read.js +49 -0
- package/src/tools/recorder.js +74 -0
- package/src/tools/web.js +84 -0
- package/src/tools/write.js +48 -0
- package/src/update.js +82 -0
- package/src/user-tools.js +59 -0
- package/src/wakeups.js +40 -0
|
@@ -0,0 +1,1263 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { makeEvent } from './events.js'
|
|
5
|
+
import { createSession, forkSession, openSession, loadSession, listSessions, deleteSession, appendSessionEvent, onSessionWriteError } from './session.js'
|
|
6
|
+
import { createContextTracker } from './context.js'
|
|
7
|
+
import { deriveState, userEntries, rewindStats } from './derive.js'
|
|
8
|
+
import { appendPrompt } from './history.js'
|
|
9
|
+
import { runTurn, summarizeText, compactHistory, compactProgress } from './agent.js'
|
|
10
|
+
import { createAgentManager } from './agents.js'
|
|
11
|
+
import { runDeliberation, validateDeliberation } from './deliberation.js'
|
|
12
|
+
import { deliberationsFromEvents } from './deliberation-history.js'
|
|
13
|
+
import { compactionPrompt, formatCompactSummary, summarySections, compactionKeepFrom } from './compaction.js'
|
|
14
|
+
import { createToolset } from './tools/index.js'
|
|
15
|
+
import { defaultTitle } from './tools/recorder.js'
|
|
16
|
+
import { scanUserTools } from './user-tools.js'
|
|
17
|
+
import { createSkillIndex } from './skills.js'
|
|
18
|
+
import { createCommandIndex } from './commands.js'
|
|
19
|
+
import { initPrompt } from './init.js'
|
|
20
|
+
import { revertEdits, reapplyEdits } from './rewind.js'
|
|
21
|
+
import { buildSystemPrompt } from './system-prompt.js'
|
|
22
|
+
import { memoryIndex } from './memory.js'
|
|
23
|
+
import { transcriptToMarkdown } from './export.js'
|
|
24
|
+
import { findModel, estimateCost } from './models.js'
|
|
25
|
+
import { adhocModel } from './catalog.js'
|
|
26
|
+
import { writeConfig } from './config.js'
|
|
27
|
+
import { connectOpenAI, openaiCredentials, disconnectOpenAI } from './openai-auth.js'
|
|
28
|
+
import { agentScratchDir, ensureDir } from './paths.js'
|
|
29
|
+
import { loadCodexModels } from './codex-models.js'
|
|
30
|
+
import { fuzzyScore } from './fuzzy.js'
|
|
31
|
+
import { finalizeUserContent, inputTextFromContent, mediaTypeFor } from './attachments.js'
|
|
32
|
+
|
|
33
|
+
export const EFFORT_LEVELS = [
|
|
34
|
+
{ key: null, desc: 'let the provider decide how much to think' },
|
|
35
|
+
{ key: 'low', desc: 'quick answers, minimal thinking' },
|
|
36
|
+
{ key: 'medium', desc: 'moderate thinking budget' },
|
|
37
|
+
{ key: 'high', desc: 'generous thinking budget' },
|
|
38
|
+
{ key: 'max', desc: 'maximum thinking budget' },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
export const SESSION_COLORS = {
|
|
42
|
+
red: '#f87171',
|
|
43
|
+
orange: '#fb923c',
|
|
44
|
+
yellow: '#facc15',
|
|
45
|
+
green: '#6BE795',
|
|
46
|
+
teal: '#2dd4bf',
|
|
47
|
+
cyan: '#22d3ee',
|
|
48
|
+
blue: '#60a5fa',
|
|
49
|
+
purple: '#a78bfa',
|
|
50
|
+
pink: '#f472b6',
|
|
51
|
+
gray: '#9ca3af',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const WORKER_TOOLS = ['read', 'write', 'edit', 'bash', 'glob', 'grep', 'shell_output', 'shell_kill', 'web_search', 'web_fetch']
|
|
55
|
+
const AGENT_TOOLS = ['agent_plan', 'agent_start', 'agent_list', 'agent_collect', 'agent_cancel']
|
|
56
|
+
const CONTEXT_COLORS = ['#67b7ff', '#c792ea', '#f7c66a', '#f78c6c', '#6be795']
|
|
57
|
+
|
|
58
|
+
function createEmitter() {
|
|
59
|
+
const listeners = new Map()
|
|
60
|
+
return {
|
|
61
|
+
on(type, fn) {
|
|
62
|
+
if (!listeners.has(type)) listeners.set(type, new Set())
|
|
63
|
+
listeners.get(type).add(fn)
|
|
64
|
+
return () => listeners.get(type)?.delete(fn)
|
|
65
|
+
},
|
|
66
|
+
emit(type, payload) {
|
|
67
|
+
for (const fn of [...(listeners.get(type) || [])]) fn(payload)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parallelPrompt(task, agentLimit) {
|
|
73
|
+
return `Use parallel agents for the following task: ${task}\n\nFirst call agent_plan. Interpret any agent-count instruction in the user's task semantically and declare that count; if the user gave no count, declare the configured default budget of ${agentLimit}. Then use agent_start within that enforced budget to delegate distinct, focused parts of the task to the configured worker model. Collect workers with agent_collect, critically evaluate their results, and synthesize the final response. When useful and the budget permits, use independent workers to check important disputed or weak conclusions. Do not delegate final synthesis. Do not emit progress updates while agents run; Pico displays agent activity automatically.`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function deliberatePrompt(decision) {
|
|
77
|
+
return `Deliberate on the following decision: ${decision}\n\nImmediately call deliberate with a self-contained brief. Do not research first, start ordinary agents, or approximate the deliberation yourself. The deliberation participants own all supporting research.`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function workerSystemPrompt(scratchpad) {
|
|
81
|
+
return `You are an isolated worker operating in the user's real project. Complete only the assigned task and do not ask the user questions. You may read, modify, and test project files. Put disposable scripts, generated data, experiments, and temporary package installs in your session-persistent scratchpad at ${scratchpad}, also available as $PICO_SCRATCHPAD. Use primary sources where possible, distinguish evidence from inference, and return a concise self-contained result.`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function participantSystemPrompt(scratchpad) {
|
|
85
|
+
return `You are one participant in an isolated, bounded deliberation. Research actively with project and web tools, prefer primary sources, and distinguish evidence from inference. Do not ask the user questions or modify project files. Your persistent scratchpad is ${scratchpad}.`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shellRuntime(shell) {
|
|
89
|
+
const secs = Math.max(0, Math.round(((shell.endedAt || Date.now()) - shell.startedAt) / 1000))
|
|
90
|
+
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function errorText(err, limit) {
|
|
94
|
+
return String(err?.message || err).slice(0, limit)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createController({ boot }) {
|
|
98
|
+
const { on, emit } = createEmitter()
|
|
99
|
+
|
|
100
|
+
const state = {
|
|
101
|
+
session: null,
|
|
102
|
+
events: [],
|
|
103
|
+
persisted: 0,
|
|
104
|
+
derived: deriveState([]),
|
|
105
|
+
model: boot.initialModel,
|
|
106
|
+
defaultModel: boot.initialModel,
|
|
107
|
+
effort: boot.initialEffort,
|
|
108
|
+
defaultEffort: boot.initialEffort,
|
|
109
|
+
busy: false,
|
|
110
|
+
compacting: false,
|
|
111
|
+
compactStatus: null,
|
|
112
|
+
turnPhase: 'idle',
|
|
113
|
+
startedAt: 0,
|
|
114
|
+
overlay: [],
|
|
115
|
+
streaming: null,
|
|
116
|
+
queued: [],
|
|
117
|
+
expedited: [],
|
|
118
|
+
sent: [],
|
|
119
|
+
question: null,
|
|
120
|
+
rewindUndo: null,
|
|
121
|
+
attachments: new Map(),
|
|
122
|
+
imageCount: 0,
|
|
123
|
+
activityVersion: 0,
|
|
124
|
+
held: false,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let abort = null
|
|
128
|
+
let sendAfterToolTriggered = false
|
|
129
|
+
let nextResearchAgentLimit = null
|
|
130
|
+
let pendingSystemNotes = []
|
|
131
|
+
const warnedTools = new Set()
|
|
132
|
+
|
|
133
|
+
const changed = () => emit('change', state)
|
|
134
|
+
const flash = (message) => emit('flash', message)
|
|
135
|
+
|
|
136
|
+
function set(patch) {
|
|
137
|
+
Object.assign(state, patch)
|
|
138
|
+
changed()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function bumpActivity() {
|
|
142
|
+
set({ activityVersion: state.activityVersion + 1 })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function persist(event) {
|
|
146
|
+
state.events.push(event)
|
|
147
|
+
if (state.session) {
|
|
148
|
+
state.session.append(event)
|
|
149
|
+
state.persisted = state.events.length
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ensureSession() {
|
|
154
|
+
if (!state.session) state.session = createSession({ cwd: boot.cwd, root: boot.root })
|
|
155
|
+
while (state.persisted < state.events.length) {
|
|
156
|
+
state.session.append(state.events[state.persisted])
|
|
157
|
+
state.persisted++
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function reDerive() {
|
|
162
|
+
state.derived = deriveState(state.events)
|
|
163
|
+
emit('derived', state.derived)
|
|
164
|
+
changed()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function codexAuth() {
|
|
168
|
+
if (state.model.provider !== 'codex') return { auth: null, ok: true }
|
|
169
|
+
const auth = await openaiCredentials().catch(() => null)
|
|
170
|
+
if (!auth) flash('codex models need a ChatGPT sign-in: run /connect')
|
|
171
|
+
return { auth, ok: !!auth }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const agents = createAgentManager({
|
|
175
|
+
concurrency: 8,
|
|
176
|
+
defaults: () => ({ model: boot.researchModel }),
|
|
177
|
+
onChange: bumpActivity,
|
|
178
|
+
onCreate: (agent) => persist(makeEvent('agent_start', { agentId: agent.id, description: agent.description, prompt: agent.prompt, model: agent.model, role: agent.role, sessionId: agent.sessionId, sessionFile: agent.sessionFile, tools: agent.tools })),
|
|
179
|
+
onEvent: (agent, event) => {
|
|
180
|
+
if (['tool_executing', 'tool_complete', 'tool_error', 'usage'].includes(event.type)) persist(makeEvent('agent_event', { agentId: agent.id, event }))
|
|
181
|
+
},
|
|
182
|
+
onFinish: (agent) => {
|
|
183
|
+
persist(makeEvent('agent_result', { agentId: agent.id, result: agent.result, usage: agent.usage, interrupted: agent.status === 'cancelled', error: agent.error }))
|
|
184
|
+
noteSystem(`Agent ${agent.id} (${agent.description}) finished with status ${agent.status}. Its result is ready. Call agent_collect with id ${agent.id} before finishing your response.`, { wake: true, agentId: agent.id, sessionId: agent.sessionId, sessionFile: agent.sessionFile })
|
|
185
|
+
},
|
|
186
|
+
// this closure outlives any single boot: /connect and project switches
|
|
187
|
+
// both replace boot fields, so it reads the live boot every run
|
|
188
|
+
run: async (agent, signal, onStream) => {
|
|
189
|
+
const worker = boot.models.find((m) => m.name === agent.model)
|
|
190
|
+
if (!worker || worker.available === false) throw new Error(`research model unavailable: ${agent.model}`)
|
|
191
|
+
const auth = worker.provider === 'codex' ? await openaiCredentials() : null
|
|
192
|
+
const sessionId = state.session?.id
|
|
193
|
+
if (!sessionId) throw new Error('worker requires an active session')
|
|
194
|
+
const scratchpad = ensureDir(agentScratchDir(boot.root, sessionId, agent.id))
|
|
195
|
+
const requestedTools = agent.tools?.length ? agent.tools.filter((name) => WORKER_TOOLS.includes(name)) : WORKER_TOOLS
|
|
196
|
+
const { tools, recorder } = createToolset({
|
|
197
|
+
cwd: boot.cwd,
|
|
198
|
+
env: { PICO_SCRATCHPAD: scratchpad },
|
|
199
|
+
// a private tracker seeded from the main one: a worker may read an
|
|
200
|
+
// AGENTS.md the main agent has not seen, and consuming it from the
|
|
201
|
+
// shared set would mean the main agent never receives it
|
|
202
|
+
tracker: createContextTracker({ stopDir: boot.startupContext.stopDir, loaded: new Set(boot.tracker.loaded) }),
|
|
203
|
+
shells: boot.shells,
|
|
204
|
+
sessionId,
|
|
205
|
+
sessionFile: state.session?.file,
|
|
206
|
+
dredge: boot.dredge,
|
|
207
|
+
signal,
|
|
208
|
+
maxToolCalls: 30,
|
|
209
|
+
allowNames: requestedTools,
|
|
210
|
+
})
|
|
211
|
+
return runTurn({
|
|
212
|
+
history: [{ role: 'user', content: agent.prompt }],
|
|
213
|
+
tools,
|
|
214
|
+
recorder,
|
|
215
|
+
modelName: worker.name,
|
|
216
|
+
effort: worker.effort ? 'low' : null,
|
|
217
|
+
auth,
|
|
218
|
+
system: workerSystemPrompt(scratchpad),
|
|
219
|
+
signal,
|
|
220
|
+
onStream,
|
|
221
|
+
})
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const deliberations = {
|
|
226
|
+
run: async ({ brief, rounds, signal }) => {
|
|
227
|
+
const options = validateDeliberation({ brief, rounds })
|
|
228
|
+
brief = options.brief
|
|
229
|
+
rounds = options.rounds
|
|
230
|
+
const existingIds = deliberationsFromEvents(state.events).map((item) => Number(item.deliberationId)).filter(Number.isFinite)
|
|
231
|
+
const id = String(Math.max(0, ...existingIds) + 1)
|
|
232
|
+
const modelName = boot.deliberationModel
|
|
233
|
+
const worker = boot.models.find((m) => m.name === modelName)
|
|
234
|
+
if (!worker || worker.available === false) throw new Error(`deliberation model unavailable: ${modelName}`)
|
|
235
|
+
const auth = worker.provider === 'codex' ? await openaiCredentials() : null
|
|
236
|
+
const sessionId = state.session?.id
|
|
237
|
+
if (!sessionId) throw new Error('deliberation requires an active session')
|
|
238
|
+
persist(makeEvent('deliberation_start', { deliberationId: id, brief, rounds, model: modelName }))
|
|
239
|
+
bumpActivity()
|
|
240
|
+
|
|
241
|
+
const persistDeliberation = (event) => {
|
|
242
|
+
persist(event)
|
|
243
|
+
bumpActivity()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const runWorker = async ({ history, role, tools: enabled = true, onStream }) => {
|
|
247
|
+
const scratchpad = ensureDir(agentScratchDir(boot.root, sessionId, `deliberation-${id}-${role}`))
|
|
248
|
+
const toolset = createToolset({
|
|
249
|
+
cwd: boot.cwd,
|
|
250
|
+
env: { PICO_SCRATCHPAD: scratchpad },
|
|
251
|
+
tracker: createContextTracker({ stopDir: boot.startupContext.stopDir, loaded: new Set(boot.tracker.loaded) }),
|
|
252
|
+
shells: boot.shells,
|
|
253
|
+
sessionId,
|
|
254
|
+
sessionFile: state.session?.file,
|
|
255
|
+
dredge: boot.dredge,
|
|
256
|
+
signal,
|
|
257
|
+
maxToolCalls: 30,
|
|
258
|
+
allowNames: enabled ? WORKER_TOOLS : [],
|
|
259
|
+
})
|
|
260
|
+
return runTurn({
|
|
261
|
+
history,
|
|
262
|
+
tools: toolset.tools,
|
|
263
|
+
recorder: toolset.recorder,
|
|
264
|
+
modelName,
|
|
265
|
+
effort: worker.effort ? 'low' : null,
|
|
266
|
+
auth,
|
|
267
|
+
system: participantSystemPrompt(scratchpad),
|
|
268
|
+
signal,
|
|
269
|
+
onStream,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = await runDeliberation({
|
|
274
|
+
brief,
|
|
275
|
+
rounds,
|
|
276
|
+
signal,
|
|
277
|
+
runParticipant: ({ history, role, round }) => runWorker({
|
|
278
|
+
history,
|
|
279
|
+
role,
|
|
280
|
+
onStream: (event) => {
|
|
281
|
+
if (['tool_executing', 'tool_complete', 'tool_error'].includes(event.type)) {
|
|
282
|
+
persistDeliberation(makeEvent('deliberation_event', { deliberationId: id, role, round, event }))
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
runSynthesis: ({ history }) => runWorker({ history, role: 'synthesizer', tools: false }),
|
|
287
|
+
onEvent: (event) => persistDeliberation(makeEvent('deliberation_turn', { deliberationId: id, ...event })),
|
|
288
|
+
})
|
|
289
|
+
persistDeliberation(makeEvent('deliberation_result', { deliberationId: id, result: result.result, usage: result.usage, interrupted: result.interrupted, error: result.error }))
|
|
290
|
+
if (result.error) throw new Error(result.error)
|
|
291
|
+
return {
|
|
292
|
+
deliberationId: id,
|
|
293
|
+
synthesis: result.result,
|
|
294
|
+
interrupted: result.interrupted,
|
|
295
|
+
review: 'The full deliberation transcript and tool activity are available in the session activity panel.',
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
onSessionWriteError((err) => flash(`session not saved: ${errorText(err, 80)}`))
|
|
301
|
+
boot.setMcpNotify(() => emit('mcp', boot.mcp.list()))
|
|
302
|
+
boot.setShellsNotify(() => emit('shells'))
|
|
303
|
+
boot.setWakeupsNotify(() => emit('shells'))
|
|
304
|
+
boot.setGitNotify(() => emit('git'))
|
|
305
|
+
boot.setWakeupsFire((wakeup) => {
|
|
306
|
+
flash(`wake-up ${wakeup.id} fired`)
|
|
307
|
+
noteSystem(`[system notification] scheduled wake-up ${wakeup.id} fired. Note to self: ${wakeup.note}`, { wake: true })
|
|
308
|
+
})
|
|
309
|
+
boot.setShellsExit((shell) => {
|
|
310
|
+
if (shell.killedBy === 'model') {
|
|
311
|
+
flash(`shell ${shell.id} killed`)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
if (shell.killedBy === 'user') {
|
|
315
|
+
flash(`shell ${shell.id} killed`)
|
|
316
|
+
noteSystem(`[system notification] the user manually killed background shell ${shell.id} (${shell.description || shell.command}) from the shells panel (SIGTERM). This was deliberate; do not restart it unless asked.`, { wake: false, sessionId: shell.sessionId, sessionFile: shell.sessionFile })
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
flash(`shell ${shell.id} exited · code ${shell.exitCode}`)
|
|
320
|
+
const tail = boot.shells.output(shell.id, { tail: 30 }).output
|
|
321
|
+
noteSystem(
|
|
322
|
+
`[system notification] background shell ${shell.id} (${shell.description || shell.command}) exited with code ${shell.exitCode} after ${shellRuntime(shell)}.` +
|
|
323
|
+
(tail ? `\nRecent output:\n${tail}` : ''),
|
|
324
|
+
{ wake: true, sessionId: shell.sessionId, sessionFile: shell.sessionFile },
|
|
325
|
+
)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
function noteSystem(text, { wake, agentId, sessionId = state.session?.id, sessionFile = state.session?.file } = {}) {
|
|
329
|
+
pendingSystemNotes.push({ text, wake, agentId, sessionId, sessionFile })
|
|
330
|
+
flushSystemNotes()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function discardCollectedAgentNotes(agentIds) {
|
|
334
|
+
const collected = new Set(agentIds.map(String))
|
|
335
|
+
pendingSystemNotes = pendingSystemNotes.filter((note) => !collected.has(String(note.agentId)))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function flushSystemNotes() {
|
|
339
|
+
if (!pendingSystemNotes.length || state.busy || state.held || !state.session) return
|
|
340
|
+
pendingSystemNotes = pendingSystemNotes.filter((note) => !note.agentId || !agents.get(note.agentId)?.collectedAt)
|
|
341
|
+
const currentSessionId = state.session.id
|
|
342
|
+
const current = pendingSystemNotes.filter((note) => !note.sessionId || note.sessionId === currentSessionId)
|
|
343
|
+
const elsewhere = pendingSystemNotes.filter((note) => note.sessionId && note.sessionId !== currentSessionId)
|
|
344
|
+
pendingSystemNotes = []
|
|
345
|
+
for (const notes of Map.groupBy(elsewhere, (note) => note.sessionId).values()) {
|
|
346
|
+
const sessionFile = notes.find((note) => note.sessionFile)?.sessionFile
|
|
347
|
+
if (sessionFile) appendSessionEvent(sessionFile, makeEvent('system_note', { text: notes.map((n) => n.text).join('\n\n') }))
|
|
348
|
+
}
|
|
349
|
+
if (!current.length) return
|
|
350
|
+
persist(makeEvent('system_note', { text: current.map((n) => n.text).join('\n\n') }))
|
|
351
|
+
reDerive()
|
|
352
|
+
if (current.some((n) => n.wake)) runAgentTurn()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function hold(held) {
|
|
356
|
+
set({ held })
|
|
357
|
+
if (!held) flushSystemNotes()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function flushStream(items) {
|
|
361
|
+
if (state.streaming) items.push({ kind: 'assistant', text: state.streaming })
|
|
362
|
+
state.streaming = null
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function executeTurn(text) {
|
|
366
|
+
const { content } = finalizeUserContent(text, state.attachments)
|
|
367
|
+
persist(makeEvent('message', { message: { role: 'user', content } }))
|
|
368
|
+
ensureSession()
|
|
369
|
+
reDerive()
|
|
370
|
+
await runAgentTurn()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function takePending() {
|
|
374
|
+
const next = [...state.expedited, ...state.queued]
|
|
375
|
+
set({ expedited: [], queued: [] })
|
|
376
|
+
return next
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function compact(instructions = '') {
|
|
380
|
+
if (state.busy || state.compacting) return flash('finish or interrupt the current turn first')
|
|
381
|
+
const current = state.derived
|
|
382
|
+
if (current.providerHistory.length < 4) return flash('nothing to compact yet')
|
|
383
|
+
|
|
384
|
+
const controller = new AbortController()
|
|
385
|
+
abort = controller
|
|
386
|
+
set({ busy: true, compacting: true, compactStatus: null, startedAt: Date.now() })
|
|
387
|
+
|
|
388
|
+
const { auth, ok } = await codexAuth()
|
|
389
|
+
if (!ok) {
|
|
390
|
+
abort = null
|
|
391
|
+
set({ compacting: false, busy: false })
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const keepFrom = compactionKeepFrom(current, state.model.context)
|
|
396
|
+
let streamed = ''
|
|
397
|
+
try {
|
|
398
|
+
const raw = await compactHistory({
|
|
399
|
+
history: current.providerHistory,
|
|
400
|
+
modelName: state.model.name,
|
|
401
|
+
auth,
|
|
402
|
+
prompt: compactionPrompt(instructions),
|
|
403
|
+
signal: controller.signal,
|
|
404
|
+
onStream: (event) => {
|
|
405
|
+
if (event.type !== 'content') return
|
|
406
|
+
streamed += event.content
|
|
407
|
+
set({ compactStatus: compactProgress(streamed) })
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
const summary = formatCompactSummary(raw)
|
|
411
|
+
if (!summary) throw new Error('empty summary')
|
|
412
|
+
if (summarySections(summary) < 5) throw new Error('malformed summary, conversation left untouched')
|
|
413
|
+
persist(makeEvent('compact', { summary, keepFrom, sessionFile: state.session?.file || null }))
|
|
414
|
+
reDerive()
|
|
415
|
+
flash('compacted · recent messages kept verbatim')
|
|
416
|
+
} catch (err) {
|
|
417
|
+
if (controller.signal.aborted) flash('compaction cancelled')
|
|
418
|
+
else flash(`compact failed: ${errorText(err, 100)}`)
|
|
419
|
+
} finally {
|
|
420
|
+
abort = null
|
|
421
|
+
set({ compacting: false, compactStatus: null, busy: false })
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (state.expedited.length > 0 || state.queued.length > 0) {
|
|
425
|
+
const next = takePending()
|
|
426
|
+
if (controller.signal.aborted) emit('input', next.join('\n'))
|
|
427
|
+
else {
|
|
428
|
+
executeTurn(next.join('\n'))
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
flushSystemNotes()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function maybeAutoCompact() {
|
|
436
|
+
if (boot.autoCompact === false || state.busy || state.compacting) return
|
|
437
|
+
const limit = state.model.context
|
|
438
|
+
const used = state.derived.lastPromptTokens
|
|
439
|
+
if (!limit || !used || state.derived.lastPromptModel !== state.model.name) return
|
|
440
|
+
const ratio = used / limit
|
|
441
|
+
if (ratio >= 0.85) {
|
|
442
|
+
flash(`context ${Math.round(ratio * 100)}% full · auto-compacting`)
|
|
443
|
+
compact()
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function refreshProjectIndexes() {
|
|
448
|
+
boot.skills = await createSkillIndex(boot.root).catch(() => boot.skills) ?? boot.skills
|
|
449
|
+
boot.commands = await createCommandIndex(boot.root).catch(() => boot.commands) ?? boot.commands
|
|
450
|
+
const scan = await scanUserTools({ cwd: boot.cwd, root: boot.root }).catch(() => ({ tools: [], errors: [] }))
|
|
451
|
+
for (const failure of scan.errors) {
|
|
452
|
+
const key = `${failure.file}:${failure.error}`
|
|
453
|
+
if (warnedTools.has(key)) continue
|
|
454
|
+
warnedTools.add(key)
|
|
455
|
+
flash(`tool skipped: ${failure.file.split('/').pop()} · ${failure.error}`)
|
|
456
|
+
}
|
|
457
|
+
return scan
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function askUser(questions) {
|
|
461
|
+
return new Promise((resolve) => {
|
|
462
|
+
set({ question: { questions, resolve } })
|
|
463
|
+
emit('question', state.question)
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function updateOverlay(fn) {
|
|
468
|
+
state.overlay = fn(state.overlay)
|
|
469
|
+
changed()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function onToolUpdate(pending) {
|
|
473
|
+
if (pending.name !== 'bash') return
|
|
474
|
+
updateOverlay((items) => items.map((item) =>
|
|
475
|
+
item.kind === 'tool' && item.callId === pending.callId
|
|
476
|
+
? { ...item, fullOutput: pending.fullOutput, outputLineStart: pending.outputLineStart, outputLineCount: pending.outputLineCount }
|
|
477
|
+
: item,
|
|
478
|
+
))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function streamHandler(recorder, controller) {
|
|
482
|
+
return (event) => {
|
|
483
|
+
if (event.type === 'thinking') {
|
|
484
|
+
updateOverlay((items) => {
|
|
485
|
+
const next = [...items]
|
|
486
|
+
flushStream(next)
|
|
487
|
+
const last = next.at(-1)
|
|
488
|
+
if (last?.kind === 'thoughts') last.text += event.content
|
|
489
|
+
else next.push({ kind: 'thoughts', text: event.content })
|
|
490
|
+
return next
|
|
491
|
+
})
|
|
492
|
+
set({ turnPhase: 'thinking' })
|
|
493
|
+
} else if (event.type === 'content') {
|
|
494
|
+
set({ turnPhase: 'responding', streaming: (state.streaming || '') + event.content })
|
|
495
|
+
} else if (event.type === 'tool_calls_ready') {
|
|
496
|
+
set({ turnPhase: 'tools' })
|
|
497
|
+
updateOverlay((items) => {
|
|
498
|
+
const next = [...items]
|
|
499
|
+
flushStream(next)
|
|
500
|
+
return next
|
|
501
|
+
})
|
|
502
|
+
} else if (event.type === 'tool_executing') {
|
|
503
|
+
updateOverlay((items) => {
|
|
504
|
+
const next = [...items]
|
|
505
|
+
flushStream(next)
|
|
506
|
+
let args = {}
|
|
507
|
+
try {
|
|
508
|
+
args = JSON.parse(event.call.function.arguments)
|
|
509
|
+
} catch {}
|
|
510
|
+
next.push({
|
|
511
|
+
kind: 'tool',
|
|
512
|
+
callId: event.call.id,
|
|
513
|
+
name: event.call.function.name,
|
|
514
|
+
description: args.description,
|
|
515
|
+
title: defaultTitle(event.call.function.name, args),
|
|
516
|
+
titleLang: event.call.function.name === 'bash' ? 'bash' : null,
|
|
517
|
+
status: 'running',
|
|
518
|
+
startedAt: Date.now(),
|
|
519
|
+
})
|
|
520
|
+
return next
|
|
521
|
+
})
|
|
522
|
+
} else if (event.type === 'tool_complete' || event.type === 'tool_error') {
|
|
523
|
+
boot.git.refresh()
|
|
524
|
+
const entry = recorder.entries.at(-1)
|
|
525
|
+
updateOverlay((items) =>
|
|
526
|
+
items.map((item) =>
|
|
527
|
+
item.kind === 'tool' && item.callId === event.call.id
|
|
528
|
+
? { ...item, ...(entry?.callId === event.call.id ? entry : {}), kind: 'tool', status: entry?.status || 'done' }
|
|
529
|
+
: item,
|
|
530
|
+
),
|
|
531
|
+
)
|
|
532
|
+
if (state.expedited.length > 0) {
|
|
533
|
+
sendAfterToolTriggered = true
|
|
534
|
+
controller.abort()
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function runAgentTurn() {
|
|
541
|
+
const researchAgentLimit = nextResearchAgentLimit || null
|
|
542
|
+
nextResearchAgentLimit = null
|
|
543
|
+
emit('turn', 'start')
|
|
544
|
+
set({ busy: true, turnPhase: 'responding', startedAt: Date.now() })
|
|
545
|
+
|
|
546
|
+
const { auth, ok } = await codexAuth()
|
|
547
|
+
if (!ok) {
|
|
548
|
+
set({ turnPhase: 'idle', busy: false })
|
|
549
|
+
flushSystemNotes()
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const controller = new AbortController()
|
|
554
|
+
abort = controller
|
|
555
|
+
const { tracker } = boot
|
|
556
|
+
const loadedBefore = new Set(tracker.loaded)
|
|
557
|
+
const userToolScan = await refreshProjectIndexes()
|
|
558
|
+
const freshSkills = boot.skills
|
|
559
|
+
const { tools, recorder } = createToolset({
|
|
560
|
+
cwd: boot.cwd,
|
|
561
|
+
tracker,
|
|
562
|
+
skills: freshSkills,
|
|
563
|
+
shells: boot.shells,
|
|
564
|
+
sessionId: state.session?.id,
|
|
565
|
+
sessionFile: state.session?.file,
|
|
566
|
+
wakeups: boot.wakeups,
|
|
567
|
+
memory: boot.memory,
|
|
568
|
+
agents: boot.researchModel ? agents : null,
|
|
569
|
+
deliberations: boot.deliberationModel ? deliberations : null,
|
|
570
|
+
onAgentsCollected: discardCollectedAgentNotes,
|
|
571
|
+
askUser,
|
|
572
|
+
dredge: boot.dredge,
|
|
573
|
+
mcpTools: boot.mcp.tools(),
|
|
574
|
+
userTools: userToolScan.tools,
|
|
575
|
+
signal: controller.signal,
|
|
576
|
+
maxAgentStarts: researchAgentLimit ? 100 : undefined,
|
|
577
|
+
requireAgentPlan: !!researchAgentLimit,
|
|
578
|
+
allowNames: researchAgentLimit ? AGENT_TOOLS : undefined,
|
|
579
|
+
onToolUpdate,
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
sendAfterToolTriggered = false
|
|
583
|
+
let result
|
|
584
|
+
try {
|
|
585
|
+
result = await runTurn({
|
|
586
|
+
history: state.derived.providerHistory,
|
|
587
|
+
tools,
|
|
588
|
+
recorder,
|
|
589
|
+
modelName: state.model.name,
|
|
590
|
+
effort: effortApplies() ? state.effort ?? 'auto' : null,
|
|
591
|
+
auth,
|
|
592
|
+
system: buildSystemPrompt({
|
|
593
|
+
cwd: boot.cwd,
|
|
594
|
+
contextFiles: boot.startupContext.files,
|
|
595
|
+
skills: freshSkills.list(),
|
|
596
|
+
memoryIndexText: memoryIndex(await boot.memory.list().catch(() => []), boot.root),
|
|
597
|
+
}),
|
|
598
|
+
signal: controller.signal,
|
|
599
|
+
onStream: streamHandler(recorder, controller),
|
|
600
|
+
})
|
|
601
|
+
} catch (err) {
|
|
602
|
+
abort = null
|
|
603
|
+
set({ overlay: [], streaming: null, turnPhase: 'idle', busy: false })
|
|
604
|
+
flash(`error: ${errorText(err, 120)}`)
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const turnTranscript = [...state.overlay]
|
|
609
|
+
flushStream(turnTranscript)
|
|
610
|
+
for (const message of result.messages) persist(makeEvent('message', { message, hideFromTranscript: true }))
|
|
611
|
+
persist(makeEvent('turn_transcript', { items: turnTranscript }))
|
|
612
|
+
for (const entry of recorder.entries) persist(makeEvent('tool_meta', entry))
|
|
613
|
+
for (const path of tracker.loaded) {
|
|
614
|
+
if (!loadedBefore.has(path)) persist(makeEvent('context_file', { path }))
|
|
615
|
+
}
|
|
616
|
+
if (result.usage) persist(makeEvent('usage', { model: state.model.name, usage: result.usage, lastPrompt: result.lastPromptTokens }))
|
|
617
|
+
if (result.interrupted) persist(makeEvent('interrupt', {}))
|
|
618
|
+
|
|
619
|
+
abort = null
|
|
620
|
+
state.overlay = []
|
|
621
|
+
state.streaming = null
|
|
622
|
+
state.turnPhase = 'idle'
|
|
623
|
+
state.busy = false
|
|
624
|
+
reDerive()
|
|
625
|
+
boot.git.refresh()
|
|
626
|
+
if (result.stalled) {
|
|
627
|
+
flash('model stalled · turn interrupted')
|
|
628
|
+
noteSystem(
|
|
629
|
+
'[system notification] the previous turn was cut off automatically: the model produced no output for 5 minutes (provider stall). Work may have stopped mid-task; pick up where it left off.',
|
|
630
|
+
{ wake: false },
|
|
631
|
+
)
|
|
632
|
+
} else if (result.error) {
|
|
633
|
+
flash(`error: ${result.error.slice(0, 120)}`)
|
|
634
|
+
noteSystem(
|
|
635
|
+
`[system notification] the previous turn ended with a provider error: ${result.error.slice(0, 300)}. Work may have stopped mid-task; pick up where it left off.`,
|
|
636
|
+
{ wake: false },
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const expeditedMessages = state.expedited
|
|
641
|
+
const pendingMessages = state.queued
|
|
642
|
+
if (expeditedMessages.length > 0 || pendingMessages.length > 0) {
|
|
643
|
+
set({ expedited: [], queued: [] })
|
|
644
|
+
if (result.interrupted && !sendAfterToolTriggered) {
|
|
645
|
+
emit('input', [...expeditedMessages, ...pendingMessages].join('\n'))
|
|
646
|
+
} else {
|
|
647
|
+
const next = sendAfterToolTriggered ? expeditedMessages : [...expeditedMessages, ...pendingMessages]
|
|
648
|
+
if (sendAfterToolTriggered && pendingMessages.length > 0) set({ queued: pendingMessages })
|
|
649
|
+
executeTurn(next.join('\n'))
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
flushSystemNotes()
|
|
654
|
+
maybeAutoCompact()
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function send(text) {
|
|
658
|
+
const value = text.trim()
|
|
659
|
+
if (!value) return
|
|
660
|
+
state.sent = [...state.sent, { text: value, at: Date.now() }]
|
|
661
|
+
appendPrompt(boot.root, value).catch(() => {})
|
|
662
|
+
if (state.busy) {
|
|
663
|
+
set({ queued: [...state.queued, value] })
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
changed()
|
|
667
|
+
executeTurn(value)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function interrupt() {
|
|
671
|
+
if (!state.busy) return
|
|
672
|
+
sendAfterToolTriggered = false
|
|
673
|
+
cancelQuestion()
|
|
674
|
+
abort?.abort()
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function answerQuestion(answers) {
|
|
678
|
+
const request = state.question
|
|
679
|
+
if (!request) return
|
|
680
|
+
set({ question: null })
|
|
681
|
+
request.resolve({ answers })
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function cancelQuestion() {
|
|
685
|
+
const request = state.question
|
|
686
|
+
if (!request) return
|
|
687
|
+
set({ question: null })
|
|
688
|
+
request.resolve({ cancelled: true })
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function recallPending() {
|
|
692
|
+
const next = takePending()
|
|
693
|
+
return next.join('\n')
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function expediteQueued() {
|
|
697
|
+
set({ expedited: [...state.expedited, ...state.queued], queued: [] })
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function resetConversation({ model, effort } = {}) {
|
|
701
|
+
state.session = null
|
|
702
|
+
state.events = []
|
|
703
|
+
state.persisted = 0
|
|
704
|
+
state.rewindUndo = null
|
|
705
|
+
state.queued = []
|
|
706
|
+
state.expedited = []
|
|
707
|
+
state.sent = []
|
|
708
|
+
state.model = model ?? state.defaultModel
|
|
709
|
+
state.effort = effort === undefined ? state.defaultEffort : effort
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function newSession() {
|
|
713
|
+
if (state.busy) return flash('finish or interrupt the current turn first')
|
|
714
|
+
resetConversation()
|
|
715
|
+
agents.clear()
|
|
716
|
+
reDerive()
|
|
717
|
+
emit('session', state.session)
|
|
718
|
+
flash('new session')
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function deleteCurrentSession() {
|
|
722
|
+
const { session } = state
|
|
723
|
+
if (!session) return false
|
|
724
|
+
if (state.busy) {
|
|
725
|
+
flash('finish or interrupt the current turn first')
|
|
726
|
+
return false
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
await session.flush()
|
|
730
|
+
await deleteSession(session.file)
|
|
731
|
+
} catch (err) {
|
|
732
|
+
flash(`delete failed: ${errorText(err, 80)}`)
|
|
733
|
+
return false
|
|
734
|
+
}
|
|
735
|
+
resetConversation()
|
|
736
|
+
reDerive()
|
|
737
|
+
emit('session', state.session)
|
|
738
|
+
flash('session deleted')
|
|
739
|
+
return true
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function fork(label) {
|
|
743
|
+
if (!label) return flash('usage: /fork <label>')
|
|
744
|
+
if (state.busy) return flash('finish or interrupt the current turn first')
|
|
745
|
+
ensureSession()
|
|
746
|
+
const forked = await forkSession({ source: state.session, cwd: boot.cwd, root: boot.root, events: state.events, label })
|
|
747
|
+
state.session = forked.session
|
|
748
|
+
state.events = forked.events
|
|
749
|
+
state.persisted = forked.events.length
|
|
750
|
+
state.rewindUndo = null
|
|
751
|
+
state.queued = []
|
|
752
|
+
state.expedited = []
|
|
753
|
+
state.sent = []
|
|
754
|
+
agents.restore(forked.events)
|
|
755
|
+
reDerive()
|
|
756
|
+
emit('session', state.session)
|
|
757
|
+
flash(`forked session as "${label}"`)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function rename(text) {
|
|
761
|
+
const automaticTitle = userEntries(state.derived)[0]?.text.trim().slice(0, 200)
|
|
762
|
+
persist(makeEvent('title', { text: text || null }))
|
|
763
|
+
ensureSession()
|
|
764
|
+
reDerive()
|
|
765
|
+
if (text) flash(`session renamed to "${text}"`)
|
|
766
|
+
else if (automaticTitle) flash(`session name reset to "${automaticTitle}"`)
|
|
767
|
+
else flash('session name reset')
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function setColor(input = '') {
|
|
771
|
+
const names = Object.keys(SESSION_COLORS)
|
|
772
|
+
const values = Object.values(SESSION_COLORS)
|
|
773
|
+
let value
|
|
774
|
+
if (!input) {
|
|
775
|
+
value = values[(values.indexOf(state.derived.color) + 1) % values.length]
|
|
776
|
+
} else {
|
|
777
|
+
value = SESSION_COLORS[input.toLowerCase()] || (/^#[0-9a-fA-F]{6}$/.test(input) ? input : null)
|
|
778
|
+
if (!value) return flash(`usage: /color to cycle, or /color <${names.join('|')}|#hex>`)
|
|
779
|
+
}
|
|
780
|
+
persist(makeEvent('color', { value }))
|
|
781
|
+
ensureSession()
|
|
782
|
+
reDerive()
|
|
783
|
+
flash(`session color: ${names[values.indexOf(value)] || value}`)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function clear() {
|
|
787
|
+
if (state.busy) return flash('finish or interrupt the current turn first')
|
|
788
|
+
persist(makeEvent('clear', {}))
|
|
789
|
+
reDerive()
|
|
790
|
+
flash('conversation cleared')
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function recallText(entry) {
|
|
794
|
+
return inputTextFromContent(entry.content ?? entry.text, {
|
|
795
|
+
attachments: state.attachments,
|
|
796
|
+
nextId: () => ++state.imageCount,
|
|
797
|
+
})
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function attachImage(path) {
|
|
801
|
+
const mediaType = mediaTypeFor(path)
|
|
802
|
+
if (!mediaType) return null
|
|
803
|
+
const placeholder = `[Image #${++state.imageCount}]`
|
|
804
|
+
state.attachments.set(placeholder, { path, mediaType })
|
|
805
|
+
return placeholder
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function attachProjectFile(file) {
|
|
809
|
+
const full = join(boot.cwd, file)
|
|
810
|
+
if (!mediaTypeFor(file) || !existsSync(full)) return null
|
|
811
|
+
return attachImage(full)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function detachImage(placeholder) {
|
|
815
|
+
state.attachments.delete(placeholder)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function restoreModelFromSession() {
|
|
819
|
+
const name = state.derived.model
|
|
820
|
+
const catalogMatch = boot.models.find((m) => m.name === name && m.available !== false)
|
|
821
|
+
const restored = name && (catalogMatch || adhocModel(name, boot.providers))
|
|
822
|
+
if (restored) {
|
|
823
|
+
state.model = restored
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
state.model = state.defaultModel
|
|
827
|
+
if (name) flash(`model ${name} unavailable, using ${state.defaultModel.name}`)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async function resume(meta) {
|
|
831
|
+
if (state.busy) return flash('finish or interrupt the current turn before switching sessions')
|
|
832
|
+
if (meta.header.root !== boot.root) return switchProject(meta)
|
|
833
|
+
try {
|
|
834
|
+
const { header, events } = await loadSession(meta.file)
|
|
835
|
+
state.session = openSession({ file: meta.file, header })
|
|
836
|
+
state.events = [...events]
|
|
837
|
+
state.persisted = events.length
|
|
838
|
+
state.rewindUndo = null
|
|
839
|
+
agents.restore(events)
|
|
840
|
+
reDerive()
|
|
841
|
+
restoreModelFromSession()
|
|
842
|
+
state.effort = state.derived.effort === undefined ? state.defaultEffort : state.derived.effort
|
|
843
|
+
state.sent = userEntries(state.derived).map((e) => ({ text: recallText(e), at: header.createdAt }))
|
|
844
|
+
changed()
|
|
845
|
+
emit('session', state.session)
|
|
846
|
+
emit('resumed', meta)
|
|
847
|
+
} catch (err) {
|
|
848
|
+
flash(`resume failed: ${errorText(err, 80)}`)
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function listProjects() {
|
|
853
|
+
const metas = await listSessions({ scope: 'everywhere', root: boot.root })
|
|
854
|
+
const byRoot = new Map()
|
|
855
|
+
for (const m of metas) {
|
|
856
|
+
const entry = byRoot.get(m.header.root)
|
|
857
|
+
if (entry) {
|
|
858
|
+
entry.sessions.push(m)
|
|
859
|
+
entry.count++
|
|
860
|
+
continue
|
|
861
|
+
}
|
|
862
|
+
byRoot.set(m.header.root, { root: m.header.root, latest: m, sessions: [m], count: 1, current: m.header.root === boot.root })
|
|
863
|
+
}
|
|
864
|
+
return [...byRoot.values()]
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function switchProject(meta) {
|
|
868
|
+
if (state.busy) return flash('finish or interrupt the current turn first')
|
|
869
|
+
try {
|
|
870
|
+
const previousMcp = boot.mcp
|
|
871
|
+
const next = await boot.rebuild(meta.header.root)
|
|
872
|
+
previousMcp.closeAll().catch(() => {})
|
|
873
|
+
process.chdir(next.cwd)
|
|
874
|
+
Object.assign(boot, next)
|
|
875
|
+
boot.git.retarget(next.root)
|
|
876
|
+
next.mcp.connectAll()
|
|
877
|
+
emit('mcp', next.mcp.list())
|
|
878
|
+
set({ queued: [], expedited: [] })
|
|
879
|
+
emit('project', boot)
|
|
880
|
+
await resume(meta)
|
|
881
|
+
flash(`switched to ${next.displayCwd}`)
|
|
882
|
+
} catch (err) {
|
|
883
|
+
flash(`switch failed: ${errorText(err, 80)}`)
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function resolveModel(name) {
|
|
888
|
+
const exact = boot.models.find((m) => m.name === name)
|
|
889
|
+
if (exact) return exact
|
|
890
|
+
if (name.includes('/')) return adhocModel(name, boot.providers)
|
|
891
|
+
const scored = boot.models
|
|
892
|
+
.map((m) => [fuzzyScore(name, m.name), m])
|
|
893
|
+
.filter(([score]) => score >= 0)
|
|
894
|
+
.sort((a, b) => b[0] - a[0])
|
|
895
|
+
return scored[0]?.[1] || null
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function switchModel(pick, { asDefault = false, note = '' } = {}) {
|
|
899
|
+
if (!pick) return false
|
|
900
|
+
if (pick.available === false) {
|
|
901
|
+
flash(`set ${pick.keyHint} in your environment to use ${pick.name}`)
|
|
902
|
+
return false
|
|
903
|
+
}
|
|
904
|
+
persist(makeEvent('model_switch', { from: state.model.name, to: pick.name }))
|
|
905
|
+
state.model = pick
|
|
906
|
+
if (asDefault) {
|
|
907
|
+
state.defaultModel = pick
|
|
908
|
+
writeConfig({ defaultModel: pick.name }).catch(() => {})
|
|
909
|
+
}
|
|
910
|
+
changed()
|
|
911
|
+
flash(`model set to ${pick.name}${note}${asDefault ? ' · saved as default' : ''}`)
|
|
912
|
+
return true
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function switchModelByName(name) {
|
|
916
|
+
const pick = resolveModel(name)
|
|
917
|
+
if (!pick) return flash(`no available model matches "${name}"`)
|
|
918
|
+
const adhoc = !boot.models.includes(pick)
|
|
919
|
+
switchModel(pick, { note: adhoc ? ' (not in catalog, pricing unknown)' : '' })
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const effortApplies = () => !!state.model.effort
|
|
923
|
+
|
|
924
|
+
function setEffort(next, { asDefault = false } = {}) {
|
|
925
|
+
persist(makeEvent('effort', { to: next }))
|
|
926
|
+
state.effort = next
|
|
927
|
+
if (asDefault) {
|
|
928
|
+
state.defaultEffort = next
|
|
929
|
+
writeConfig({ defaultEffort: next }).catch(() => {})
|
|
930
|
+
}
|
|
931
|
+
changed()
|
|
932
|
+
flash(`effort: ${next ?? 'default'}${asDefault ? ' · saved as default' : ''}`)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function setEffortByName(level) {
|
|
936
|
+
if (!effortApplies()) return flash(`${state.model.name} does not support effort control`)
|
|
937
|
+
if (level === 'default') return setEffort(null)
|
|
938
|
+
if (!EFFORT_LEVELS.some((l) => l.key === level)) return flash('usage: /effort <default|low|medium|high|max>')
|
|
939
|
+
setEffort(level)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function modelAvailable(name) {
|
|
943
|
+
return !!name && boot.models.some((m) => m.name === name && m.available !== false)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function sendParallel(task) {
|
|
947
|
+
if (!task) {
|
|
948
|
+
flash('usage: /parallel <task>')
|
|
949
|
+
return true
|
|
950
|
+
}
|
|
951
|
+
if (!modelAvailable(boot.researchModel)) return false
|
|
952
|
+
nextResearchAgentLimit = boot.researchAgentLimit
|
|
953
|
+
send(parallelPrompt(task, boot.researchAgentLimit))
|
|
954
|
+
return true
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function sendDeliberate(decision) {
|
|
958
|
+
if (!decision) {
|
|
959
|
+
flash('usage: /deliberate <decision>')
|
|
960
|
+
return true
|
|
961
|
+
}
|
|
962
|
+
if (!modelAvailable(boot.deliberationModel)) return false
|
|
963
|
+
send(deliberatePrompt(decision))
|
|
964
|
+
return true
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async function sendSkill(name) {
|
|
968
|
+
const body = await boot.skills.load(name)
|
|
969
|
+
if (!body) return flash(`could not load skill ${name}`)
|
|
970
|
+
persist(makeEvent('skill', { name, source: 'user' }))
|
|
971
|
+
send(`Follow these skill instructions now.\n\n${body}`)
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async function sendCommand(name, args) {
|
|
975
|
+
const text = await boot.commands.load(name, args)
|
|
976
|
+
if (!text) return flash(`could not load command ${name}`)
|
|
977
|
+
send(text)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function sendInit(args) {
|
|
981
|
+
send(initPrompt(args))
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function previewSteer(changes) {
|
|
985
|
+
if (!changes?.length) return state.derived
|
|
986
|
+
return deriveState([...state.events, { id: '__steer_preview__', at: Date.now(), type: 'steer', data: { changes } }])
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function applySteer(changes) {
|
|
990
|
+
if (!changes?.length) return
|
|
991
|
+
persist(makeEvent('steer', { changes }))
|
|
992
|
+
reDerive()
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async function rewind(target, mode) {
|
|
996
|
+
const current = state.derived
|
|
997
|
+
const { edits } = rewindStats(current, target.index)
|
|
998
|
+
const editsLabel = `${edits.length} ${edits.length === 1 ? 'edit' : 'edits'}`
|
|
999
|
+
let reverted = []
|
|
1000
|
+
let skipped = []
|
|
1001
|
+
|
|
1002
|
+
if (mode === 'both' || mode === 'code') {
|
|
1003
|
+
const result = await revertEdits(edits)
|
|
1004
|
+
reverted = result.reverted
|
|
1005
|
+
skipped = result.skipped
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
let summaryText = null
|
|
1009
|
+
if (mode === 'summary') {
|
|
1010
|
+
const tail = current.transcript.slice(target.index)
|
|
1011
|
+
const text = tail.filter((m) => m.text).map((m) => `${m.kind}: ${m.text}`).join('\n')
|
|
1012
|
+
flash('summarizing...')
|
|
1013
|
+
const { auth } = await codexAuth()
|
|
1014
|
+
summaryText = await summarizeText({ text, modelName: state.model.name, auth }).catch(() => {
|
|
1015
|
+
flash('summary model call failed · kept a crude digest of the rewound turns')
|
|
1016
|
+
return tail.filter((m) => m.text).slice(0, 3).map((m) => m.text.split(/\s+/).slice(0, 6).join(' ')).join(' · ')
|
|
1017
|
+
})
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const event = makeEvent('rewind', { target: target.eventId, mode, summaryText, reverted, skipped })
|
|
1021
|
+
persist(event)
|
|
1022
|
+
state.rewindUndo = { rewindId: event.id, edits: edits.filter((e) => reverted.includes(e.callId)) }
|
|
1023
|
+
reDerive()
|
|
1024
|
+
if (mode !== 'code') emit('input', recallText(target))
|
|
1025
|
+
|
|
1026
|
+
const skippedNote = skipped.length ? ` · skipped ${skipped.length} drifted` : ''
|
|
1027
|
+
if (mode === 'code') flash(`reverted ${editsLabel}, conversation kept${skippedNote} · ctrl+z to undo`)
|
|
1028
|
+
else if (mode === 'summary') flash(`rewound and summarized${skippedNote} · ctrl+z to undo`)
|
|
1029
|
+
else if (mode === 'both') flash(`rewound, ${editsLabel} reverted${skippedNote} · ctrl+z to undo`)
|
|
1030
|
+
else flash(`rewound, file changes kept · ctrl+z to undo`)
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function undoRewind() {
|
|
1034
|
+
const undo = state.rewindUndo
|
|
1035
|
+
if (!undo) return
|
|
1036
|
+
const { skipped } = await reapplyEdits(undo.edits)
|
|
1037
|
+
persist(makeEvent('rewind_undo', { rewindId: undo.rewindId }))
|
|
1038
|
+
state.rewindUndo = null
|
|
1039
|
+
reDerive()
|
|
1040
|
+
flash(skipped.length ? `rewind undone · ${skipped.length} file(s) drifted` : 'rewind undone')
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function costSummary() {
|
|
1044
|
+
const current = state.derived
|
|
1045
|
+
const entries = Object.entries(current.usageByModel)
|
|
1046
|
+
if (entries.length === 0) return null
|
|
1047
|
+
const costOf = (byModel) =>
|
|
1048
|
+
Object.entries(byModel).reduce((sum, [name, usage]) => sum + estimateCost(findModel(boot.models, name), usage), 0)
|
|
1049
|
+
return {
|
|
1050
|
+
spent: costOf(current.usageByModel),
|
|
1051
|
+
active: costOf(current.usageActiveByModel),
|
|
1052
|
+
promptTokens: current.usage.promptTokens,
|
|
1053
|
+
completionTokens: current.usage.completionTokens,
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async function exportMarkdown(file = join(boot.cwd, `pico-export-${Date.now()}.md`)) {
|
|
1058
|
+
await writeFile(file, transcriptToMarkdown(state.derived.transcript, { title: `pico session · ${boot.cwd}` }))
|
|
1059
|
+
return file
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function baseToolset(extra = {}) {
|
|
1063
|
+
return createToolset({
|
|
1064
|
+
cwd: boot.cwd,
|
|
1065
|
+
tracker: boot.tracker,
|
|
1066
|
+
skills: boot.skills,
|
|
1067
|
+
shells: boot.shells,
|
|
1068
|
+
wakeups: boot.wakeups,
|
|
1069
|
+
memory: boot.memory,
|
|
1070
|
+
dredge: boot.dredge,
|
|
1071
|
+
...extra,
|
|
1072
|
+
})
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async function describeTools() {
|
|
1076
|
+
const scan = await scanUserTools({ cwd: boot.cwd, root: boot.root }).catch(() => ({ tools: [], errors: [] }))
|
|
1077
|
+
const { tools: builtins } = baseToolset()
|
|
1078
|
+
return {
|
|
1079
|
+
mcpCount: boot.mcp.tools().length,
|
|
1080
|
+
rows: [
|
|
1081
|
+
...builtins.map((t) => ({ name: t.name, desc: t.description.split('\n')[0], note: 'builtin' })),
|
|
1082
|
+
...scan.tools.map((t) => ({ name: t.name, desc: t.description, note: `${t.source} · ${t._file.split('/').pop()}` })),
|
|
1083
|
+
...scan.errors.map((e) => ({ name: e.file.split('/').pop(), desc: e.error, note: 'broken' })),
|
|
1084
|
+
],
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function contextBreakdown() {
|
|
1089
|
+
const est = (text) => Math.round(String(text).length / 4)
|
|
1090
|
+
const current = state.derived
|
|
1091
|
+
const memoryIndexText = memoryIndex(await boot.memory.list().catch(() => []), boot.root)
|
|
1092
|
+
const skillList = boot.skills.list()
|
|
1093
|
+
const files = boot.startupContext.files
|
|
1094
|
+
const systemFull = buildSystemPrompt({ cwd: boot.cwd, contextFiles: files, skills: skillList, memoryIndexText })
|
|
1095
|
+
const systemBase = buildSystemPrompt({ cwd: boot.cwd, contextFiles: [], skills: [], memoryIndexText: '' })
|
|
1096
|
+
const userToolScan = await scanUserTools({ cwd: boot.cwd, root: boot.root }).catch(() => ({ tools: [], errors: [] }))
|
|
1097
|
+
const { tools } = baseToolset({ mcpTools: boot.mcp.tools(), userTools: userToolScan.tools })
|
|
1098
|
+
const toolTokens = est(JSON.stringify(tools))
|
|
1099
|
+
|
|
1100
|
+
const history = current.providerHistory
|
|
1101
|
+
const compacted = history[0]?.role === 'user'
|
|
1102
|
+
&& current.historyEventIds[0] === null
|
|
1103
|
+
&& String(history[0].content).startsWith('[system notification] The earlier portion')
|
|
1104
|
+
const summaryTokens = compacted ? est(history[0].content) : 0
|
|
1105
|
+
const messages = compacted ? history.slice(1) : history
|
|
1106
|
+
const messageTokens = est(JSON.stringify(messages))
|
|
1107
|
+
|
|
1108
|
+
const rows = [
|
|
1109
|
+
{ name: 'system prompt', desc: 'identity, environment, tool guidance', tokens: est(systemBase) },
|
|
1110
|
+
{ name: `tool schemas (${tools.length})`, desc: tools.map((t) => t.name).join(' · '), tokens: toolTokens },
|
|
1111
|
+
]
|
|
1112
|
+
if (files.length) {
|
|
1113
|
+
rows.push({
|
|
1114
|
+
name: `project instructions (${files.length})`,
|
|
1115
|
+
desc: files.map((f) => f.path.replace(`${boot.root}/`, '')).join(', '),
|
|
1116
|
+
tokens: files.reduce((sum, f) => sum + est(f.content), 0),
|
|
1117
|
+
})
|
|
1118
|
+
}
|
|
1119
|
+
if (skillList.length) {
|
|
1120
|
+
rows.push({
|
|
1121
|
+
name: `skills index (${skillList.length})`,
|
|
1122
|
+
desc: skillList.map((s) => s.name).join(', '),
|
|
1123
|
+
tokens: est(skillList.map((s) => `- ${s.name}: ${s.description}`).join('\n')),
|
|
1124
|
+
})
|
|
1125
|
+
}
|
|
1126
|
+
if (memoryIndexText) rows.push({ name: 'memory index', desc: 'one line per saved memory', tokens: est(memoryIndexText) })
|
|
1127
|
+
if (compacted) rows.push({ name: 'compaction summary', desc: 'stands in for everything before the last compact', tokens: summaryTokens })
|
|
1128
|
+
rows.push({
|
|
1129
|
+
name: `conversation (${messages.length} messages)`,
|
|
1130
|
+
desc: compacted ? 'kept verbatim since the last compact' : 'every message this session',
|
|
1131
|
+
tokens: messageTokens,
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
const measured = current.lastPromptTokens && current.lastPromptModel === state.model.name ? current.lastPromptTokens : null
|
|
1135
|
+
|
|
1136
|
+
const segments = [
|
|
1137
|
+
{ label: 'system', tokens: est(systemBase) },
|
|
1138
|
+
{ label: 'tools', tokens: toolTokens },
|
|
1139
|
+
{ label: 'project', tokens: Math.max(0, est(systemFull) - est(systemBase)) },
|
|
1140
|
+
...(summaryTokens ? [{ label: 'summary', tokens: summaryTokens }] : []),
|
|
1141
|
+
{ label: 'conversation', tokens: messageTokens },
|
|
1142
|
+
].filter((segment) => segment.tokens > 0)
|
|
1143
|
+
.map((segment, i) => ({ ...segment, color: CONTEXT_COLORS[i] }))
|
|
1144
|
+
|
|
1145
|
+
return { model: state.model.name, rows, measured, segments }
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function connectProvider() {
|
|
1149
|
+
const { email } = await connectOpenAI()
|
|
1150
|
+
boot.providers = [...new Set([...boot.providers, 'codex'])]
|
|
1151
|
+
const creds = await openaiCredentials().catch(() => null)
|
|
1152
|
+
const codex = (await loadCodexModels(creds)).map((m) => ({ ...m, available: true, keyHint: '/connect' }))
|
|
1153
|
+
boot.models = [...boot.models.filter((m) => m.provider !== 'codex'), ...codex]
|
|
1154
|
+
reDerive()
|
|
1155
|
+
return { email, count: codex.length }
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
async function disconnectProvider() {
|
|
1159
|
+
await disconnectOpenAI().catch(() => {})
|
|
1160
|
+
boot.providers = boot.providers.filter((p) => p !== 'codex')
|
|
1161
|
+
boot.models = boot.models.map((m) => (m.provider === 'codex' ? { ...m, available: false } : m))
|
|
1162
|
+
if (state.model.provider === 'codex') state.model = state.defaultModel
|
|
1163
|
+
changed()
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function activity() {
|
|
1167
|
+
const rows = [...agents.list(), ...deliberationsFromEvents(state.events)]
|
|
1168
|
+
return rows.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function cancelAgent(id) {
|
|
1172
|
+
agents.cancel(id)
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function dismissAgent(id) {
|
|
1176
|
+
if (!agents.dismiss(id)) return false
|
|
1177
|
+
persist(makeEvent('agent_dismiss', { agentId: id }))
|
|
1178
|
+
return true
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function dismissDeliberation(deliberationId) {
|
|
1182
|
+
persist(makeEvent('deliberation_dismiss', { deliberationId }))
|
|
1183
|
+
bumpActivity()
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function shellRows() {
|
|
1187
|
+
return boot.shells.list()
|
|
1188
|
+
.filter((shell) => shell.sessionId === state.session?.id)
|
|
1189
|
+
.sort((a, b) => Number(b.id) - Number(a.id))
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function cancelWakeup(wakeup) {
|
|
1193
|
+
boot.wakeups.cancel(wakeup.id)
|
|
1194
|
+
flash(`cancelled wake-up ${wakeup.id}`)
|
|
1195
|
+
noteSystem(
|
|
1196
|
+
`[system notification] the user cancelled scheduled wake-up ${wakeup.id} (note was: ${wakeup.note.replace(/\n/g, ' ')}). This was deliberate; do not reschedule it unless asked.`,
|
|
1197
|
+
{ wake: false },
|
|
1198
|
+
)
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async function shutdown() {
|
|
1202
|
+
await state.session?.flush()
|
|
1203
|
+
boot.shells.killAll()
|
|
1204
|
+
boot.mcp.terminateAll()
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return {
|
|
1208
|
+
state,
|
|
1209
|
+
boot,
|
|
1210
|
+
on,
|
|
1211
|
+
agents,
|
|
1212
|
+
send,
|
|
1213
|
+
interrupt,
|
|
1214
|
+
compact,
|
|
1215
|
+
answerQuestion,
|
|
1216
|
+
cancelQuestion,
|
|
1217
|
+
recallPending,
|
|
1218
|
+
expediteQueued,
|
|
1219
|
+
hold,
|
|
1220
|
+
noteSystem,
|
|
1221
|
+
newSession,
|
|
1222
|
+
deleteCurrentSession,
|
|
1223
|
+
fork,
|
|
1224
|
+
rename,
|
|
1225
|
+
setColor,
|
|
1226
|
+
clear,
|
|
1227
|
+
resume,
|
|
1228
|
+
listProjects,
|
|
1229
|
+
switchProject,
|
|
1230
|
+
resolveModel,
|
|
1231
|
+
switchModel,
|
|
1232
|
+
switchModelByName,
|
|
1233
|
+
effortApplies,
|
|
1234
|
+
setEffort,
|
|
1235
|
+
setEffortByName,
|
|
1236
|
+
sendParallel,
|
|
1237
|
+
sendDeliberate,
|
|
1238
|
+
sendSkill,
|
|
1239
|
+
sendCommand,
|
|
1240
|
+
sendInit,
|
|
1241
|
+
previewSteer,
|
|
1242
|
+
applySteer,
|
|
1243
|
+
rewind,
|
|
1244
|
+
undoRewind,
|
|
1245
|
+
recallText,
|
|
1246
|
+
attachImage,
|
|
1247
|
+
attachProjectFile,
|
|
1248
|
+
detachImage,
|
|
1249
|
+
costSummary,
|
|
1250
|
+
exportMarkdown,
|
|
1251
|
+
describeTools,
|
|
1252
|
+
contextBreakdown,
|
|
1253
|
+
connectProvider,
|
|
1254
|
+
disconnectProvider,
|
|
1255
|
+
activity,
|
|
1256
|
+
cancelAgent,
|
|
1257
|
+
dismissAgent,
|
|
1258
|
+
dismissDeliberation,
|
|
1259
|
+
shellRows,
|
|
1260
|
+
cancelWakeup,
|
|
1261
|
+
shutdown,
|
|
1262
|
+
}
|
|
1263
|
+
}
|