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.
Files changed (57) hide show
  1. package/README.md +15 -0
  2. package/package.json +33 -0
  3. package/src/agent-transcript.js +80 -0
  4. package/src/agent.js +170 -0
  5. package/src/agents.js +213 -0
  6. package/src/attachments.js +141 -0
  7. package/src/boot.js +28 -0
  8. package/src/catalog-snapshot.json +1 -0
  9. package/src/catalog.js +86 -0
  10. package/src/codex-models.js +56 -0
  11. package/src/commands.js +61 -0
  12. package/src/compaction.js +82 -0
  13. package/src/completion.js +21 -0
  14. package/src/config.js +32 -0
  15. package/src/context.js +82 -0
  16. package/src/controller.js +1263 -0
  17. package/src/conversation-search.js +101 -0
  18. package/src/deliberation-history.js +65 -0
  19. package/src/deliberation.js +61 -0
  20. package/src/derive.js +307 -0
  21. package/src/events.js +54 -0
  22. package/src/export.js +16 -0
  23. package/src/files.js +39 -0
  24. package/src/format.js +6 -0
  25. package/src/fuzzy.js +41 -0
  26. package/src/git.js +156 -0
  27. package/src/history.js +51 -0
  28. package/src/init.js +25 -0
  29. package/src/keys.js +26 -0
  30. package/src/mcp.js +280 -0
  31. package/src/memory.js +120 -0
  32. package/src/models.js +14 -0
  33. package/src/openai-auth.js +204 -0
  34. package/src/paths.js +67 -0
  35. package/src/reversible-edit.js +79 -0
  36. package/src/rewind.js +84 -0
  37. package/src/session-index.js +264 -0
  38. package/src/session-lock.js +27 -0
  39. package/src/session.js +160 -0
  40. package/src/shells.js +166 -0
  41. package/src/skills.js +164 -0
  42. package/src/steer.js +129 -0
  43. package/src/system-prompt.js +49 -0
  44. package/src/terminal-theme.js +49 -0
  45. package/src/tools/bash.js +184 -0
  46. package/src/tools/diff.js +18 -0
  47. package/src/tools/edit.js +95 -0
  48. package/src/tools/glob.js +43 -0
  49. package/src/tools/grep.js +59 -0
  50. package/src/tools/index.js +296 -0
  51. package/src/tools/read.js +49 -0
  52. package/src/tools/recorder.js +74 -0
  53. package/src/tools/web.js +84 -0
  54. package/src/tools/write.js +48 -0
  55. package/src/update.js +82 -0
  56. package/src/user-tools.js +59 -0
  57. package/src/wakeups.js +40 -0
@@ -0,0 +1,264 @@
1
+ import { writeFileSync } from 'node:fs'
2
+ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises'
3
+ import { dirname, join } from 'node:path'
4
+ import { streamEvents } from './events.js'
5
+ import { withIndexLock, withSessionLock } from './session-lock.js'
6
+
7
+ const INDEX_VERSION = 4
8
+ const INDEX_FILE = 'index.json'
9
+ const DIRTY_SUFFIX = '.dirty'
10
+ const DELETED_SUFFIX = '.jsonl.deleted'
11
+ const FLUSH_DELAY_MS = 1000
12
+
13
+ const dirtyFiles = new Set()
14
+ const pendingEvents = new Map()
15
+ let flushTimer = null
16
+ let indexWrites = Promise.resolve()
17
+
18
+ function messageText(content) {
19
+ if (typeof content === 'string') return content
20
+ if (!Array.isArray(content)) return ''
21
+ return content.filter((part) => part.type === 'text').map((part) => part.text).join('')
22
+ }
23
+
24
+ function applyEvent(meta, event) {
25
+ const next = { ...meta }
26
+ if (event.type === 'message' && ['user', 'assistant'].includes(event.data?.message?.role)) {
27
+ const { role, content } = event.data.message
28
+ const text = messageText(content).trim()
29
+ if (text) {
30
+ next.previewMessageCount++
31
+ next.preview = [...next.preview, { role, text }]
32
+ const userIndexes = next.preview.flatMap((message, index) => message.role === 'user' ? [index] : [])
33
+ if (userIndexes.length > 2) next.preview = next.preview.slice(userIndexes.at(-2))
34
+ }
35
+ if (role !== 'user') return next
36
+ next.turns++
37
+ if (!next.automaticTitle) {
38
+ next.automaticTitle = text.slice(0, 200) || null
39
+ if (!next.customTitle) next.title = next.automaticTitle || next.title
40
+ }
41
+ }
42
+ if (event.type === 'title') {
43
+ next.customTitle = event.data.text
44
+ next.title = event.data.text || next.automaticTitle || 'Untitled session'
45
+ }
46
+ if (event.type === 'color') next.color = event.data.value
47
+ return next
48
+ }
49
+
50
+ async function metadataFromFile(file) {
51
+ let meta = null
52
+ for await (const event of streamEvents(file)) {
53
+ if (!meta) {
54
+ if (event.type !== 'session') throw new Error('invalid session')
55
+ meta = {
56
+ file,
57
+ header: event,
58
+ title: 'Untitled session',
59
+ automaticTitle: null,
60
+ customTitle: undefined,
61
+ color: null,
62
+ turns: 0,
63
+ preview: [],
64
+ previewMessageCount: 0,
65
+ at: 0,
66
+ }
67
+ } else {
68
+ meta = applyEvent(meta, event)
69
+ }
70
+ }
71
+ if (!meta) throw new Error('invalid session')
72
+ meta.at = (await stat(file)).mtimeMs
73
+ return meta
74
+ }
75
+
76
+ function indexPath(dir) {
77
+ return join(dir, INDEX_FILE)
78
+ }
79
+
80
+ async function readIndex(dir) {
81
+ const parsed = JSON.parse(await readFile(indexPath(dir), 'utf-8'))
82
+ if (parsed.version !== INDEX_VERSION || !Array.isArray(parsed.sessions)) throw new Error('invalid session index')
83
+ return parsed.sessions
84
+ }
85
+
86
+ async function writeIndex(dir, sessions) {
87
+ await mkdir(dir, { recursive: true })
88
+ const file = indexPath(dir)
89
+ const temporary = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`
90
+ try {
91
+ await writeFile(temporary, JSON.stringify({ version: INDEX_VERSION, sessions }) + '\n')
92
+ await rename(temporary, file)
93
+ } finally {
94
+ await rm(temporary, { force: true })
95
+ }
96
+ }
97
+
98
+ function dirtyPath(file) {
99
+ return `${file}${DIRTY_SUFFIX}`
100
+ }
101
+
102
+ function fileFromDirtyName(dir, name) {
103
+ return join(dir, name.slice(0, -DIRTY_SUFFIX.length))
104
+ }
105
+
106
+ function enqueueIndexWrite(task) {
107
+ const result = indexWrites.then(task)
108
+ indexWrites = result.catch(() => {})
109
+ return result
110
+ }
111
+
112
+ async function rebuildIndex(dir, names) {
113
+ if (!names) {
114
+ try {
115
+ names = await readdir(dir)
116
+ } catch {
117
+ return []
118
+ }
119
+ }
120
+ const files = names.filter((name) => name.endsWith('.jsonl')).map((name) => join(dir, name))
121
+ const sessions = (await Promise.all(files.map((file) => metadataFromFile(file).catch(() => null)))).filter(Boolean)
122
+ await writeIndex(dir, sessions)
123
+ return sessions
124
+ }
125
+
126
+ async function refreshFiles(dir, sessions, files, eventsByFile = new Map()) {
127
+ for (const file of files) {
128
+ try {
129
+ const at = sessions.findIndex((session) => session.file === file)
130
+ const events = eventsByFile.get(file)
131
+ let row
132
+ if (at >= 0 && events?.length) {
133
+ const { mtimeMs } = await stat(file)
134
+ row = events.reduce(applyEvent, { ...sessions[at], at: mtimeMs })
135
+ } else {
136
+ row = await withSessionLock(file, () => metadataFromFile(file))
137
+ }
138
+ if (at < 0) sessions.push(row)
139
+ else sessions[at] = row
140
+ } catch (err) {
141
+ if (err.code === 'ENOENT') sessions = sessions.filter((session) => session.file !== file)
142
+ else throw err
143
+ }
144
+ }
145
+ await writeIndex(dir, sessions)
146
+ await Promise.all(files.map((file) => rm(dirtyPath(file), { force: true })))
147
+ return sessions
148
+ }
149
+
150
+ async function currentSessions(dir) {
151
+ const names = await readdir(dir)
152
+ let sessions
153
+ try {
154
+ sessions = await readIndex(dir)
155
+ } catch {
156
+ return rebuildIndex(dir, names)
157
+ }
158
+
159
+ const dirty = names.filter((name) => name.endsWith(`.jsonl${DIRTY_SUFFIX}`)).map((name) => fileFromDirtyName(dir, name))
160
+ if (dirty.length > 0) {
161
+ const files = [...new Set(dirty)]
162
+ sessions = await refreshFiles(dir, sessions, files)
163
+ files.forEach((file) => pendingEvents.delete(file))
164
+ }
165
+
166
+ const deleted = new Set(names.filter((name) => name.endsWith(DELETED_SUFFIX)).map((name) => join(dir, name.slice(0, -'.deleted'.length))))
167
+ if (deleted.size > 0) {
168
+ const filtered = sessions.filter((session) => !deleted.has(session.file))
169
+ if (filtered.length !== sessions.length) await writeIndex(dir, filtered)
170
+ sessions = filtered
171
+ }
172
+ return sessions
173
+ }
174
+
175
+ export function indexedSessions(dir) {
176
+ return enqueueIndexWrite(() => withIndexLock(dir, () => currentSessions(dir))).catch(async (err) => {
177
+ if (err.code === 'ENOENT') return []
178
+ throw err
179
+ })
180
+ }
181
+
182
+ export function markSessionIndexDirty(file, onError = () => {}) {
183
+ dirtyFiles.add(file)
184
+ try {
185
+ writeFileSync(dirtyPath(file), '')
186
+ } catch (err) {
187
+ onError(err)
188
+ }
189
+ }
190
+
191
+ function scheduleFlush() {
192
+ if (flushTimer) return
193
+ flushTimer = setTimeout(() => {
194
+ flushTimer = null
195
+ flushSessionIndex().catch(() => {})
196
+ }, FLUSH_DELAY_MS)
197
+ flushTimer.unref?.()
198
+ }
199
+
200
+ export function scheduleSessionIndexUpdate(file, event) {
201
+ dirtyFiles.add(file)
202
+ if (event) {
203
+ const events = pendingEvents.get(file) || []
204
+ events.push(event)
205
+ pendingEvents.set(file, events)
206
+ }
207
+ scheduleFlush()
208
+ }
209
+
210
+ export function flushSessionIndex() {
211
+ if (dirtyFiles.size === 0) return indexWrites
212
+ const files = [...dirtyFiles]
213
+ const eventsByFile = new Map(files.map((file) => [file, pendingEvents.get(file)]))
214
+ dirtyFiles.clear()
215
+ files.forEach((file) => pendingEvents.delete(file))
216
+ clearTimeout(flushTimer)
217
+ flushTimer = null
218
+ const byDir = Map.groupBy(files, dirname)
219
+ return enqueueIndexWrite(async () => {
220
+ const failed = []
221
+ await Promise.all([...byDir].map(async ([dir, projectFiles]) => {
222
+ try {
223
+ await withIndexLock(dir, async () => {
224
+ let sessions
225
+ let rebuilt = false
226
+ try {
227
+ sessions = await readIndex(dir)
228
+ } catch {
229
+ sessions = await rebuildIndex(dir)
230
+ rebuilt = true
231
+ }
232
+ await refreshFiles(dir, sessions, projectFiles, rebuilt ? new Map() : eventsByFile)
233
+ })
234
+ } catch (err) {
235
+ failed.push(...projectFiles)
236
+ }
237
+ }))
238
+ if (failed.length > 0) {
239
+ failed.forEach((file) => {
240
+ dirtyFiles.add(file)
241
+ const events = eventsByFile.get(file)
242
+ if (events?.length) pendingEvents.set(file, [...events, ...(pendingEvents.get(file) || [])])
243
+ })
244
+ scheduleFlush()
245
+ throw new Error('failed to update session index')
246
+ }
247
+ })
248
+ }
249
+
250
+ export function removeFromSessionIndex(file) {
251
+ dirtyFiles.delete(file)
252
+ pendingEvents.delete(file)
253
+ const dir = dirname(file)
254
+ return enqueueIndexWrite(() => withIndexLock(dir, async () => {
255
+ await rm(dirtyPath(file), { force: true })
256
+ let sessions
257
+ try {
258
+ sessions = await readIndex(dir)
259
+ } catch {
260
+ return
261
+ }
262
+ await writeIndex(dir, sessions.filter((session) => session.file !== file))
263
+ }))
264
+ }
@@ -0,0 +1,27 @@
1
+ import { mkdir } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import lockfile from 'proper-lockfile'
4
+
5
+ const lockOptions = {
6
+ realpath: false,
7
+ retries: { retries: 100, minTimeout: 10, maxTimeout: 100 },
8
+ }
9
+
10
+ async function withLock(target, lockfilePath, task) {
11
+ const release = await lockfile.lock(target, { ...lockOptions, lockfilePath })
12
+ try {
13
+ return await task()
14
+ } finally {
15
+ await release()
16
+ }
17
+ }
18
+
19
+ export async function withSessionLock(file, task) {
20
+ await mkdir(dirname(file), { recursive: true })
21
+ return withLock(file, `${file}.lock`, task)
22
+ }
23
+
24
+ export async function withIndexLock(dir, task) {
25
+ await mkdir(dir, { recursive: true })
26
+ return withLock(dir, join(dir, '.index.lock'), task)
27
+ }
package/src/session.js ADDED
@@ -0,0 +1,160 @@
1
+ import { access, appendFile, open, readdir, rm, writeFile } from 'node:fs/promises'
2
+ import { basename, dirname, join } from 'node:path'
3
+ import { picoHome, sessionsDir, ensureDir, projectDir } from './paths.js'
4
+ import { withSessionLock } from './session-lock.js'
5
+ import { flushSessionIndex, indexedSessions, markSessionIndexDirty, removeFromSessionIndex, scheduleSessionIndexUpdate } from './session-index.js'
6
+ import { makeEvent, makeHeader, readEvents, serializeLine } from './events.js'
7
+
8
+ const appendQueues = new Map()
9
+ const writeFailures = new Map()
10
+ const creationFailures = new Map()
11
+ const deletedSessions = new Set()
12
+
13
+ let onWriteError = () => {}
14
+
15
+ // appends are chained so they cannot interleave. a rejected link would poison
16
+ // every later append on that file, so failures are reported and absorbed here
17
+ // instead of propagating: one bad write must not silently end the log
18
+ export function onSessionWriteError(handler) {
19
+ onWriteError = handler || (() => {})
20
+ }
21
+
22
+ function reportWriteError(file, err) {
23
+ try {
24
+ onWriteError(err, file)
25
+ } catch {}
26
+ }
27
+
28
+ function deletedPath(file) {
29
+ return `${file}.deleted`
30
+ }
31
+
32
+ async function repairIncompleteLine(file) {
33
+ let handle
34
+ try {
35
+ handle = await open(file, 'r+')
36
+ } catch (err) {
37
+ if (err.code === 'ENOENT') return
38
+ throw err
39
+ }
40
+ try {
41
+ const { size } = await handle.stat()
42
+ if (size === 0) return
43
+ const last = Buffer.alloc(1)
44
+ await handle.read(last, 0, 1, size - 1)
45
+ if (last[0] !== 0x0a) await handle.write('\n', size)
46
+ } finally {
47
+ await handle.close()
48
+ }
49
+ }
50
+
51
+ export function appendSessionEvent(file, event) {
52
+ if (deletedSessions.has(file)) return Promise.reject(new Error(`session has been deleted: ${file}`))
53
+ markSessionIndexDirty(file, (err) => reportWriteError(file, err))
54
+ const queued = (appendQueues.get(file) || Promise.resolve()).then(async () => {
55
+ try {
56
+ await withSessionLock(file, async () => {
57
+ if (deletedSessions.has(file)) throw new Error(`session has been deleted: ${file}`)
58
+ try {
59
+ await access(deletedPath(file))
60
+ throw new Error(`session has been deleted: ${file}`)
61
+ } catch (err) {
62
+ if (err.code !== 'ENOENT') throw err
63
+ }
64
+ if (event.type === 'session') {
65
+ await writeFile(file, serializeLine(event), { flag: 'wx' })
66
+ } else {
67
+ await repairIncompleteLine(file)
68
+ await appendFile(file, serializeLine(event))
69
+ }
70
+ })
71
+ } catch (err) {
72
+ writeFailures.set(file, err)
73
+ if (event.type === 'session') creationFailures.set(file, err)
74
+ reportWriteError(file, err)
75
+ scheduleSessionIndexUpdate(file)
76
+ return
77
+ }
78
+ writeFailures.delete(file)
79
+ scheduleSessionIndexUpdate(file, event)
80
+ })
81
+ appendQueues.set(file, queued)
82
+ return queued
83
+ }
84
+
85
+ export function createSession({ cwd, root, forkedFrom }) {
86
+ const header = makeHeader({ cwd, root, forkedFrom })
87
+ const file = join(ensureDir(sessionsDir(root)), `${header.id}.jsonl`)
88
+ const session = openSession({ file, header })
89
+ session.append(header)
90
+ return session
91
+ }
92
+
93
+ export async function forkSession({ source, cwd, root, events, label }) {
94
+ await source?.flush()
95
+ const session = createSession({ cwd, root, forkedFrom: source?.id })
96
+ for (const event of events) session.append(event)
97
+ const title = makeEvent('title', { text: label })
98
+ session.append(title)
99
+ await session.flush()
100
+ return { session, events: [...events, title] }
101
+ }
102
+
103
+ export function openSession({ file, header }) {
104
+ return {
105
+ id: header.id,
106
+ file,
107
+ header,
108
+ append(event) {
109
+ return appendSessionEvent(file, event)
110
+ },
111
+ async flush() {
112
+ await (appendQueues.get(file) || Promise.resolve())
113
+ const failure = creationFailures.get(file) || writeFailures.get(file)
114
+ if (failure) throw failure
115
+ return flushSessionIndex()
116
+ },
117
+ }
118
+ }
119
+
120
+ export async function loadSession(file) {
121
+ const events = await readEvents(file)
122
+ const header = events[0]?.type === 'session' ? events.shift() : null
123
+ if (!header) throw new Error(`not a pico session file: ${file}`)
124
+ return { header, events }
125
+ }
126
+
127
+ export async function deleteSession(file) {
128
+ const sessionId = basename(file, '.jsonl')
129
+ const project = dirname(dirname(file))
130
+ deletedSessions.add(file)
131
+ await (appendQueues.get(file) || Promise.resolve())
132
+ appendQueues.delete(file)
133
+ writeFailures.delete(file)
134
+ creationFailures.delete(file)
135
+ await withSessionLock(file, async () => {
136
+ await writeFile(deletedPath(file), '')
137
+ await rm(file, { force: true })
138
+ })
139
+ await removeFromSessionIndex(file)
140
+ await rm(join(project, 'scratchpads', sessionId), { recursive: true, force: true })
141
+ }
142
+
143
+ export function deleteProjectData(root) {
144
+ return rm(projectDir(root), { recursive: true, force: true })
145
+ }
146
+
147
+ export async function listSessions({ scope, root }) {
148
+ let sessions
149
+ if (scope === 'everywhere') {
150
+ const projectsDir = join(picoHome(), 'projects')
151
+ let projects = []
152
+ try {
153
+ projects = await readdir(projectsDir)
154
+ } catch {}
155
+ sessions = (await Promise.all(projects.map((project) => indexedSessions(join(projectsDir, project, 'sessions'))))).flat()
156
+ } else {
157
+ sessions = await indexedSessions(sessionsDir(root))
158
+ }
159
+ return sessions.filter((session) => session.title !== 'Untitled session').sort((a, b) => b.at - a.at)
160
+ }
package/src/shells.js ADDED
@@ -0,0 +1,166 @@
1
+ import { spawn } from 'node:child_process'
2
+
3
+ const MAX_LINES = 2000
4
+
5
+ function killTree(child, signal) {
6
+ if (!child.pid) return false
7
+ if (process.platform !== 'win32') {
8
+ try {
9
+ process.kill(-child.pid, signal)
10
+ return true
11
+ } catch {}
12
+ }
13
+ return child.kill(signal)
14
+ }
15
+
16
+ export function createShellManager({ onChange = () => {}, onExit = () => {} } = {}) {
17
+ const shells = new Map()
18
+ let nextId = 1
19
+
20
+ function publicView(shell) {
21
+ return {
22
+ id: shell.id,
23
+ command: shell.command,
24
+ description: shell.description,
25
+ sessionId: shell.sessionId,
26
+ sessionFile: shell.sessionFile,
27
+ cwd: shell.cwd,
28
+ status: shell.status,
29
+ exitCode: shell.exitCode,
30
+ startedAt: shell.startedAt,
31
+ endedAt: shell.endedAt,
32
+ killedBy: shell.killedBy,
33
+ lastLine: shell.lines.at(-1) || '',
34
+ lineCount: shell.lines.length,
35
+ }
36
+ }
37
+
38
+ function track(child, command, { cwd, description, sessionId, sessionFile, hidden = false } = {}) {
39
+ const id = String(nextId++)
40
+ const shell = {
41
+ id,
42
+ command,
43
+ description: description || null,
44
+ sessionId: sessionId || null,
45
+ sessionFile: sessionFile || null,
46
+ cwd: cwd || process.cwd(),
47
+ child,
48
+ status: 'running',
49
+ exitCode: null,
50
+ startedAt: Date.now(),
51
+ endedAt: null,
52
+ killedBy: null,
53
+ lines: [],
54
+ partial: '',
55
+ hidden,
56
+ }
57
+
58
+ const push = (chunk) => {
59
+ const text = shell.partial + chunk.toString()
60
+ const lines = text.split('\n')
61
+ shell.partial = lines.pop() || ''
62
+ shell.lines.push(...lines)
63
+ if (shell.lines.length > MAX_LINES) shell.lines.splice(0, shell.lines.length - MAX_LINES)
64
+ if (!shell.hidden) onChange()
65
+ }
66
+ child.stdout.on('data', push)
67
+ child.stderr.on('data', push)
68
+ child.on('error', (err) => {
69
+ push(`[failed to start: ${err.message}]\n`)
70
+ })
71
+ child.on('exit', (code, signal) => {
72
+ if (shell.partial) {
73
+ shell.lines.push(shell.partial)
74
+ shell.partial = ''
75
+ }
76
+ shell.status = 'exited'
77
+ shell.exitCode = code ?? (signal ? 1 : 0)
78
+ shell.endedAt = Date.now()
79
+ if (!shell.hidden) onChange()
80
+ if (!shell.hidden) onExit(publicView(shell))
81
+ })
82
+
83
+ shells.set(id, shell)
84
+ if (!shell.hidden) onChange()
85
+ return { id }
86
+ }
87
+
88
+ function start(command, options = {}) {
89
+ const child = spawn(command, {
90
+ shell: true,
91
+ cwd: options.cwd || process.cwd(),
92
+ env: { ...process.env, ...options.env, FORCE_COLOR: '0', NO_COLOR: '1' },
93
+ stdio: ['ignore', 'pipe', 'pipe'],
94
+ detached: process.platform !== 'win32',
95
+ })
96
+ return track(child, command, options)
97
+ }
98
+
99
+ function get(id) {
100
+ const shell = shells.get(String(id))
101
+ if (!shell) throw new Error(`no shell with id ${id}; it is gone and cannot be inspected (exited shells disappear when dismissed)`)
102
+ return shell
103
+ }
104
+
105
+ return {
106
+ start,
107
+ track,
108
+ reveal(id) {
109
+ const shell = get(id)
110
+ if (shell.hidden) {
111
+ shell.hidden = false
112
+ onChange()
113
+ if (shell.status === 'exited') onExit(publicView(shell))
114
+ }
115
+ return publicView(shell)
116
+ },
117
+ discardHidden(id) {
118
+ const shell = shells.get(String(id))
119
+ if (shell?.hidden) shells.delete(String(id))
120
+ },
121
+ output(id, { tail = 100 } = {}) {
122
+ const shell = get(id)
123
+ return {
124
+ id: shell.id,
125
+ command: shell.command,
126
+ status: shell.status,
127
+ exitCode: shell.exitCode,
128
+ output: shell.lines.slice(-tail).join('\n'),
129
+ totalLines: shell.lines.length,
130
+ }
131
+ },
132
+ kill(id, by = 'model') {
133
+ const shell = get(id)
134
+ if (shell.status !== 'running') return { id: shell.id, status: shell.status, exitCode: shell.exitCode }
135
+ shell.killedBy = by
136
+ killTree(shell.child, 'SIGTERM')
137
+ setTimeout(() => {
138
+ if (shell.status === 'running') killTree(shell.child, 'SIGKILL')
139
+ }, 3000).unref()
140
+ return { id: shell.id, status: 'killing' }
141
+ },
142
+ dismiss(id) {
143
+ const shell = shells.get(String(id))
144
+ if (shell && shell.status === 'exited') {
145
+ shells.delete(String(id))
146
+ if (!shell.hidden) onChange()
147
+ }
148
+ },
149
+ list() {
150
+ return [...shells.values()].filter((shell) => !shell.hidden).map(publicView)
151
+ },
152
+ running() {
153
+ return [...shells.values()].filter((s) => s.status === 'running').length
154
+ },
155
+ killAll() {
156
+ for (const shell of shells.values()) {
157
+ if (shell.status === 'running') {
158
+ shell.killedBy = 'user'
159
+ try {
160
+ killTree(shell.child, 'SIGKILL')
161
+ } catch {}
162
+ }
163
+ }
164
+ },
165
+ }
166
+ }