picocode-core 0.9.140 → 0.9.142
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/package.json +1 -1
- package/src/attachments.js +28 -1
- package/src/controller.js +8 -7
- package/src/paths.js +4 -0
- package/src/session.js +9 -1
package/package.json
CHANGED
package/src/attachments.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
|
-
import {
|
|
2
|
+
import { copyFile, mkdir, stat } from 'node:fs/promises'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { basename, join } from 'node:path'
|
|
3
5
|
|
|
4
6
|
const MEDIA_TYPES = {
|
|
5
7
|
png: 'image/png',
|
|
@@ -146,3 +148,28 @@ export function finalizeUserContent(text, attachments, exists = existsSync) {
|
|
|
146
148
|
if (expanded.length === 1 && expanded[0].type === 'text') return { content: expanded[0].text, used }
|
|
147
149
|
return { content: expanded, used }
|
|
148
150
|
}
|
|
151
|
+
|
|
152
|
+
// images ride into a session by path, and the path is what the log keeps,
|
|
153
|
+
// so each one is copied into the session's own attachments directory
|
|
154
|
+
// before the message is written and the part points at the copy. later
|
|
155
|
+
// turns and later readers then never depend on the original still being
|
|
156
|
+
// where it was
|
|
157
|
+
async function stashImage(part, dir) {
|
|
158
|
+
const path = part.source?.path
|
|
159
|
+
if (!path || part.source.kind !== 'path' || path.startsWith(dir)) return part
|
|
160
|
+
try {
|
|
161
|
+
const { size, mtimeMs } = await stat(path)
|
|
162
|
+
const key = createHash('sha1').update(`${path}\n${size}\n${Math.floor(mtimeMs)}`).digest('hex').slice(0, 10)
|
|
163
|
+
const copy = join(dir, `${key}-${basename(path)}`)
|
|
164
|
+
await mkdir(dir, { recursive: true })
|
|
165
|
+
if (!existsSync(copy)) await copyFile(path, copy)
|
|
166
|
+
return { ...part, source: { ...part.source, path: copy } }
|
|
167
|
+
} catch {
|
|
168
|
+
return part
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function stashImages(content, dir) {
|
|
173
|
+
if (!Array.isArray(content)) return content
|
|
174
|
+
return Promise.all(content.map((part) => (part.type === 'image' ? stashImage(part, dir) : part)))
|
|
175
|
+
}
|
package/src/controller.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
|
|
|
2
2
|
import { writeFile } from 'node:fs/promises'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { makeEvent } from './events.js'
|
|
5
|
-
import { createSession, forkSession, openSession, loadSession, listSessions, deleteSession, appendSessionEvent, onSessionWriteError } from './session.js'
|
|
5
|
+
import { createSession, createEphemeralSession, forkSession, openSession, loadSession, listSessions, deleteSession, appendSessionEvent, onSessionWriteError } from './session.js'
|
|
6
6
|
import { createContextTracker } from './context.js'
|
|
7
7
|
import { deriveState, userEntries, rewindStats } from './derive.js'
|
|
8
8
|
import { appendPrompt } from './history.js'
|
|
@@ -25,11 +25,11 @@ import { findModel, estimateCost } from './models.js'
|
|
|
25
25
|
import { adhocModel } from './catalog.js'
|
|
26
26
|
import { writeConfig } from './config.js'
|
|
27
27
|
import { connectOpenAI, openaiCredentials, disconnectOpenAI } from './openai-auth.js'
|
|
28
|
-
import { agentScratchDir, ensureDir } from './paths.js'
|
|
28
|
+
import { agentScratchDir, ensureDir, sessionAttachmentsDir } from './paths.js'
|
|
29
29
|
import { loadCodexModels } from './codex-models.js'
|
|
30
30
|
import { fuzzyScore } from './fuzzy.js'
|
|
31
31
|
import { MAX_DELIBERATION_ROUNDS } from './deliberation.js'
|
|
32
|
-
import { buildUserContent, finalizeUserContent, inputTextFromContent, mediaTypeFor } from './attachments.js'
|
|
32
|
+
import { buildUserContent, finalizeUserContent, inputTextFromContent, mediaTypeFor, stashImages } from './attachments.js'
|
|
33
33
|
|
|
34
34
|
export const EFFORT_LEVELS = [
|
|
35
35
|
{ key: null, desc: 'let the provider decide how much to think' },
|
|
@@ -154,7 +154,7 @@ export function createController({ boot }) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
function ensureSession() {
|
|
157
|
-
if (!state.session) state.session = createSession({ cwd: boot.cwd, root: boot.root })
|
|
157
|
+
if (!state.session) state.session = (boot.ephemeral ? createEphemeralSession : createSession)({ cwd: boot.cwd, root: boot.root })
|
|
158
158
|
while (state.persisted < state.events.length) {
|
|
159
159
|
state.session.append(state.events[state.persisted])
|
|
160
160
|
state.persisted++
|
|
@@ -370,10 +370,11 @@ export function createController({ boot }) {
|
|
|
370
370
|
// that text must not be scanned for image paths or it attaches twice
|
|
371
371
|
const built = buildUserContent(text, state.attachments)
|
|
372
372
|
const viewed = built.used.length > 0 && built.used.every((placeholder) => deliveredImages.has(placeholder))
|
|
373
|
-
const { content, used } = viewed ? built : finalizeUserContent(text, state.attachments)
|
|
373
|
+
const { content: built2, used } = viewed ? built : finalizeUserContent(text, state.attachments)
|
|
374
374
|
for (const placeholder of used) deliveredImages.delete(placeholder)
|
|
375
|
-
persist(makeEvent('message', { message: { role: 'user', content }, ...(viewed ? { origin: 'view' } : {}) }))
|
|
376
375
|
ensureSession()
|
|
376
|
+
const content = state.session.file ? await stashImages(built2, sessionAttachmentsDir(boot.root, state.session.id)) : built2
|
|
377
|
+
persist(makeEvent('message', { message: { role: 'user', content }, ...(viewed ? { origin: 'view' } : {}) }))
|
|
377
378
|
reDerive()
|
|
378
379
|
await runAgentTurn()
|
|
379
380
|
}
|
|
@@ -738,7 +739,7 @@ export function createController({ boot }) {
|
|
|
738
739
|
}
|
|
739
740
|
try {
|
|
740
741
|
await session.flush()
|
|
741
|
-
await deleteSession(session.file)
|
|
742
|
+
if (session.file) await deleteSession(session.file)
|
|
742
743
|
} catch (err) {
|
|
743
744
|
flash(`delete failed: ${errorText(err, 80)}`)
|
|
744
745
|
return false
|
package/src/paths.js
CHANGED
|
@@ -33,6 +33,10 @@ export function sessionScratchDir(root, sessionId) {
|
|
|
33
33
|
return join(projectDir(root), 'scratchpads', sessionId)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function sessionAttachmentsDir(root, sessionId) {
|
|
37
|
+
return join(projectDir(root), 'attachments', sessionId)
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
export function agentScratchDir(root, sessionId, agentId) {
|
|
37
41
|
return join(sessionScratchDir(root, sessionId), `agent-${agentId}`)
|
|
38
42
|
}
|
package/src/session.js
CHANGED
|
@@ -90,9 +90,16 @@ export function createSession({ cwd, root, forkedFrom }) {
|
|
|
90
90
|
return session
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// an ephemeral session keeps its events in memory only: no file, no index
|
|
94
|
+
// entry, nothing to resume once it is closed
|
|
95
|
+
export function createEphemeralSession({ cwd, root, forkedFrom }) {
|
|
96
|
+
const header = makeHeader({ cwd, root, forkedFrom })
|
|
97
|
+
return { id: header.id, file: null, header, ephemeral: true, append() {}, async flush() {} }
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
export async function forkSession({ source, cwd, root, events, label }) {
|
|
94
101
|
await source?.flush()
|
|
95
|
-
const session = createSession({ cwd, root, forkedFrom: source?.id })
|
|
102
|
+
const session = source?.ephemeral ? createEphemeralSession({ cwd, root, forkedFrom: source.id }) : createSession({ cwd, root, forkedFrom: source?.id })
|
|
96
103
|
for (const event of events) session.append(event)
|
|
97
104
|
const title = makeEvent('title', { text: label })
|
|
98
105
|
session.append(title)
|
|
@@ -138,6 +145,7 @@ export async function deleteSession(file) {
|
|
|
138
145
|
})
|
|
139
146
|
await removeFromSessionIndex(file)
|
|
140
147
|
await rm(join(project, 'scratchpads', sessionId), { recursive: true, force: true })
|
|
148
|
+
await rm(join(project, 'attachments', sessionId), { recursive: true, force: true })
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
export function deleteProjectData(root) {
|