@davait/aipictury 1.19.5 → 1.20.2
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/ext/about.js +2 -2
- package/ext/accountUi.js +83 -21
- package/ext/agent.js +84 -11
- package/ext/attachments.js +55 -0
- package/ext/cloud.js +165 -32
- package/ext/dialogs.js +9 -5
- package/ext/extension.js +281 -42
- package/ext/feedback.js +78 -6
- package/ext/firestore.js +37 -6
- package/ext/gpuRun.js +31 -7
- package/ext/keeping.js +171 -0
- package/ext/mediagen.js +186 -20
- package/ext/memory.js +17 -7
- package/ext/package.json +1 -1
- package/ext/plans.js +4 -1
- package/ext/remote.js +72 -12
- package/ext/voice.js +62 -11
- package/ext/workbench.js +9 -2
- package/main.js +96 -27
- package/package.json +1 -1
- package/vscode-shim.js +3 -0
package/main.js
CHANGED
|
@@ -177,6 +177,13 @@ class Conversation {
|
|
|
177
177
|
return this.title || this.aiTitle || (first ? String(first.content).replace(/\[[^\]]*\]\n?/g, '').replace(/\s+/g, ' ').trim().slice(0, 80) : 'New conversation')
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** One row per tool call, updated in place as it runs, kept with its output. */
|
|
181
|
+
toolRow(event) {
|
|
182
|
+
const key = event.id || `${event.tool}:${JSON.stringify(event.args)}`
|
|
183
|
+
const existing = this.transcript.find((t) => t.kind === 'tool' && t.key === key)
|
|
184
|
+
if (existing) Object.assign(existing, event); else this.transcript.push({ kind: 'tool', key, at: Date.now(), ...event })
|
|
185
|
+
}
|
|
186
|
+
|
|
180
187
|
save(model) {
|
|
181
188
|
if (!this.history.some((m) => m.role === 'user')) return
|
|
182
189
|
this.store.save({ id: this.id, title: this.displayTitle, renamed: !!this.title, aiTitle: this.aiTitle || undefined, created: this.created,
|
|
@@ -200,9 +207,7 @@ async function runTurn(convo, text, ui, { model, maxTurns, content } = {}) {
|
|
|
200
207
|
onDelta: (d) => ui.delta?.(d),
|
|
201
208
|
onThinking: (d) => ui.thinking?.(d),
|
|
202
209
|
onTool: (event) => {
|
|
203
|
-
|
|
204
|
-
const existing = convo.transcript.find((t) => t.kind === 'tool' && t.key === key)
|
|
205
|
-
if (existing) Object.assign(existing, event); else convo.transcript.push({ kind: 'tool', key, at: Date.now(), ...event })
|
|
210
|
+
convo.toolRow(event)
|
|
206
211
|
ui.tool?.(event)
|
|
207
212
|
},
|
|
208
213
|
onNudge: (reason, detail) => ui.nudge?.(reason, detail),
|
|
@@ -230,6 +235,27 @@ async function runTurn(convo, text, ui, { model, maxTurns, content } = {}) {
|
|
|
230
235
|
} finally { ui.onEnd?.() }
|
|
231
236
|
}
|
|
232
237
|
|
|
238
|
+
/** What an approval asks, for a terminal: each kind the agent sends (agent.js), else the question
|
|
239
|
+
* it brings. A render's approval (kind 'media') fell through to "Use undefined {}" (1.20.0). */
|
|
240
|
+
function approvalText(request, b = (t) => t, d = (t) => t) {
|
|
241
|
+
if (request.kind === 'command') return `Run ${b(request.command)}`
|
|
242
|
+
if (request.kind === 'edit') return `${request.before == null ? 'Create' : 'Change'} ${b(request.path)}`
|
|
243
|
+
if (request.kind === 'plan-mode') return `Switch to Plan mode first${request.reason ? d(` — ${request.reason}`) : ''}`
|
|
244
|
+
if (request.tool === 'browser') return `Use the browser on ${b(request.site || 'a page')}`
|
|
245
|
+
if (request.question) {
|
|
246
|
+
// The question's first line says what and where (the verb, the file); the rest is the prompt.
|
|
247
|
+
const [head, ...rest] = String(request.question).split('\n')
|
|
248
|
+
const body = rest.join('\n').trim()
|
|
249
|
+
return `${b(head)}${request.path && !head.includes(request.path) ? ` ${d(`(${request.path})`)}` : ''}${body ? `\n${d(body.split('\n').map((l) => ` ${l}`).join('\n'))}` : ''}`
|
|
250
|
+
}
|
|
251
|
+
return `Use ${b(request.tool || request.kind || 'a tool')} ${d(JSON.stringify(request.args || {}).slice(0, 200))}`
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The one line print mode says when it refuses what needs a yes. */
|
|
255
|
+
const refusedText = (request) => `not allowed in print mode (-p): ${request.kind === 'command' ? request.command
|
|
256
|
+
: request.kind === 'media' ? `${request.question ? String(request.question).split('\n')[0] : 'a generated picture or clip'}${request.path ? ` (${request.path})` : ''}`
|
|
257
|
+
: request.path || request.tool || request.kind} — allow it with --allowedTools or --permission-mode`
|
|
258
|
+
|
|
233
259
|
// ---- print mode: one prompt, no questions ----
|
|
234
260
|
async function printMode(o, convo, text) {
|
|
235
261
|
const started = Date.now()
|
|
@@ -242,7 +268,7 @@ async function printMode(o, convo, text) {
|
|
|
242
268
|
tool: (e) => { if (events && (e.status === 'done' || e.status === 'failed' || e.status === 'refused')) emit({ type: 'tool', tool: e.tool, args: e.args, status: e.status, result: typeof e.result === 'string' ? e.result.slice(0, 4000) : undefined }) },
|
|
243
269
|
turn: ({ text: said }) => { if (events && String(said || '').trim()) emit({ type: 'assistant', text: said }) },
|
|
244
270
|
// Nobody to ask: what needs a yes is refused, a question is left to the agent.
|
|
245
|
-
approve: async (request) => ({ decision: 'deny', feedback:
|
|
271
|
+
approve: async (request) => ({ decision: 'deny', feedback: refusedText(request) }),
|
|
246
272
|
askUser: async () => ({ dismissed: true }),
|
|
247
273
|
// The plan is the answer: the run ends there, with no further turn.
|
|
248
274
|
approvePlan: async (plan) => { planText = plan; ui.controller?.abort(); return { approved: false, feedback: 'Stop here: the plan is the answer.' } },
|
|
@@ -363,10 +389,7 @@ async function interactive(o, convo, first, cfg) {
|
|
|
363
389
|
nudge: (reason, detail) => { if (detail?.text) say(dim(` · ${detail.text}`)) },
|
|
364
390
|
todos: (todos) => say(dim(todos.map((t) => ` ${t.status === 'completed' ? '☑' : t.status === 'in_progress' ? '▸' : '☐'} ${t.content}`).join('\n'))),
|
|
365
391
|
async approve(request) {
|
|
366
|
-
|
|
367
|
-
: request.kind === 'edit' ? `${request.before == null ? 'Create' : 'Change'} ${bold(request.path)}`
|
|
368
|
-
: request.tool === 'browser' ? `Use the browser on ${bold(request.site || 'a page')}` : `Use ${bold(request.tool)} ${dim(JSON.stringify(request.args || {}).slice(0, 200))}`
|
|
369
|
-
say(`\n${yellow('?')} ${what}${request.because ? dim(` — ${request.because}`) : ''}`)
|
|
392
|
+
say(`\n${yellow('?')} ${approvalText(request, bold, dim)}${request.because ? dim(` — ${request.because}`) : ''}`)
|
|
370
393
|
if (request.kind === 'edit') for (const h of (request.hunks || []).slice(0, 6)) say([...h.removed.slice(0, 8).map((l) => red(` - ${l}`)), ...h.added.slice(0, 12).map((l) => green(` + ${l}`))].join('\n'))
|
|
371
394
|
const answer = await ask(` ${bold('y')}es · ${bold('a')}lways${request.rule ? dim(` (${request.rule}, this conversation)`) : ' (this conversation)'} · ${bold('n')}o, or say what to do instead › `)
|
|
372
395
|
spinner.start()
|
|
@@ -605,6 +628,17 @@ async function interactive(o, convo, first, cfg) {
|
|
|
605
628
|
return lines.join('\n')
|
|
606
629
|
}
|
|
607
630
|
|
|
631
|
+
// A picture or clip the last run left rendering (an exit or Ctrl+C cut it): delivered now by its
|
|
632
|
+
// job id and written where it was asked for — not at the next message, nor made again (1.20.0).
|
|
633
|
+
if (convo.history.some((m) => m.pending)) {
|
|
634
|
+
say(dim(' a render the last run left going is being delivered (Esc or Ctrl+C leaves it for later)'))
|
|
635
|
+
running = new AbortController()
|
|
636
|
+
spinner.start('rendering')
|
|
637
|
+
try {
|
|
638
|
+
await agent.deliverPending(server(), convo.history, { signal: running.signal,
|
|
639
|
+
onTool: (event) => { convo.toolRow(event); ui.tool(event) }, onSettled: () => convo.save(model) })
|
|
640
|
+
} catch (error) { say(red(` not delivered: ${error.message}`)) } finally { running = null; spinner.stop() }
|
|
641
|
+
}
|
|
608
642
|
if (first) await turn(first)
|
|
609
643
|
for (;;) {
|
|
610
644
|
const message = await nextMessage()
|
|
@@ -633,7 +667,10 @@ function accountFor() {
|
|
|
633
667
|
return new Account({ store: fileStore(path.join(HOME(), 'credentials.json')), config, client: 'the aipictury CLI' })
|
|
634
668
|
}
|
|
635
669
|
|
|
636
|
-
/** The conversations of this run, kept with the account
|
|
670
|
+
/** The conversations of this run, kept with the account as VS Code keeps them: each save pushed a
|
|
671
|
+
* few seconds later (at least every half minute while a run goes on), and what is left pushed
|
|
672
|
+
* before exiting. It used to push at a clean exit only: a CLI killed, crashed or with its
|
|
673
|
+
* terminal closed left the account's copy as it was when the run began (1.20.0). */
|
|
637
674
|
function cloudFor(account) {
|
|
638
675
|
const { Cloud } = require(path.join(EXT, 'cloud.js'))
|
|
639
676
|
const { emulatorHosts } = require(path.join(EXT, 'account.js'))
|
|
@@ -643,29 +680,54 @@ function cloudFor(account) {
|
|
|
643
680
|
try { id = JSON.parse(fs.readFileSync(deviceFile, 'utf8')).id } catch { /* first run */ }
|
|
644
681
|
if (!id) { id = require('crypto').randomUUID(); fs.mkdirSync(HOME(), { recursive: true }); fs.writeFileSync(deviceFile, JSON.stringify({ id })) }
|
|
645
682
|
const cloud = new Cloud({ account, projectId: config.projectId, emulator: emulatorHosts().firestore,
|
|
646
|
-
bucket: config.storageBucket, storageEmulator: emulatorHosts().storage,
|
|
683
|
+
bucket: config.storageBucket, storageEmulator: emulatorHosts().storage, log: (line) => {
|
|
684
|
+
require(path.join(EXT, 'log.js')).warn(line)
|
|
685
|
+
if (exiting) process.stderr.write(dim(` (${line.replace(/^cloud: /, '')} — not kept with your account)\n`))
|
|
686
|
+
},
|
|
647
687
|
device: { id, name: os.hostname(), app: 'cli', platform: process.platform, version: VERSION } })
|
|
648
|
-
const
|
|
649
|
-
|
|
688
|
+
const removing = new Set()
|
|
689
|
+
let exiting = false // a push failing now is said; during the run, only logged
|
|
650
690
|
const on = (kind) => vscode.workspace.getConfiguration('aipictury').get(`cloud.${kind}`, true)
|
|
651
|
-
require(path.join(EXT, 'sessions.js'))
|
|
691
|
+
const sessions = require(path.join(EXT, 'sessions.js'))
|
|
692
|
+
sessions.onWrite((e) => {
|
|
652
693
|
if (!e.folder || !on('conversations')) return
|
|
653
|
-
if (e.type === 'remove') {
|
|
654
|
-
|
|
694
|
+
if (e.type === 'remove') {
|
|
695
|
+
cloud.cancel(e.id)
|
|
696
|
+
const gone = cloud.deleteConversation(e.folder, e.id).catch(() => {}).finally(() => removing.delete(gone))
|
|
697
|
+
removing.add(gone)
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
cloud.schedule(e.folder, { ...e.entry, origin: 'cli' }, e.type === 'patch' ? 500 : 5000).catch(() => {})
|
|
655
701
|
})
|
|
656
|
-
//
|
|
657
|
-
|
|
702
|
+
// What this folder's conversations hold that the account lacks — a run an earlier CLI was
|
|
703
|
+
// killed in, before its push — goes up at the start, as VS Code's sign-in does (cloud.bringUp).
|
|
704
|
+
let bringing = Promise.resolve()
|
|
705
|
+
const bringUp = () => {
|
|
706
|
+
if (!on('conversations')) return
|
|
707
|
+
bringing = cloud.bringUp(process.cwd(), sessions.storeFor(process.cwd()))
|
|
708
|
+
.catch((err) => process.stderr.write(dim(` (conversations not carried up to your account: ${err.message})\n`)))
|
|
709
|
+
}
|
|
710
|
+
// Every render the agent makes is kept with the account: noted in the outbox the extension shares
|
|
711
|
+
// (keeping.js), so Ctrl+C twice or a kill only delays it — the next start of either app finishes
|
|
712
|
+
// it — and what an earlier run left is taken up now. The run waits for the keeping in progress.
|
|
713
|
+
const keeping = require(path.join(EXT, 'keeping.js'))
|
|
714
|
+
const keep = () => keeping.drain(cloud, { onState: (_, state) => {
|
|
715
|
+
if (state.stage === 'failed') process.stderr.write(dim(` (render not kept with your account${state.again ? ' yet: tried again at the next start' : ''}: ${state.error})\n`))
|
|
716
|
+
} }).catch((err) => process.stderr.write(dim(` (renders not kept with your account: ${err.message})\n`)))
|
|
658
717
|
require(path.join(EXT, 'mediagen.js')).onRendered((base, jobId) => {
|
|
659
|
-
|
|
718
|
+
account.user().then((u) => { if (u) { keeping.add(base, jobId, u.uid); keep() } }).catch(() => {})
|
|
660
719
|
})
|
|
661
720
|
return {
|
|
662
721
|
cloud,
|
|
722
|
+
keep,
|
|
723
|
+
bringUp,
|
|
663
724
|
async flush() {
|
|
664
|
-
await
|
|
725
|
+
await keeping.settled()
|
|
726
|
+
await bringing
|
|
727
|
+
await Promise.allSettled([...removing])
|
|
665
728
|
if (!(await account.user())) return
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
pending.clear()
|
|
729
|
+
exiting = true
|
|
730
|
+
await cloud.flush()
|
|
669
731
|
},
|
|
670
732
|
}
|
|
671
733
|
}
|
|
@@ -725,6 +787,8 @@ async function main() {
|
|
|
725
787
|
// Every call to the GPU server carries the account's token (the server may require it), and only those.
|
|
726
788
|
const { serverFetch } = require(path.join(EXT, 'account.js'))
|
|
727
789
|
globalThis.fetch = serverFetch({ fetch: globalThis.fetch, account, server })
|
|
790
|
+
sync.keep() // renders an earlier run (or VS Code) left halfway, with the token now
|
|
791
|
+
sync.bringUp() // and conversations an earlier run left unpushed
|
|
728
792
|
// …and each of those calls is in the log (--debug: method, path, status, ms).
|
|
729
793
|
const log = require(path.join(EXT, 'log.js'))
|
|
730
794
|
log.watchFetch({ server })
|
|
@@ -774,8 +838,13 @@ async function mainRun(o) {
|
|
|
774
838
|
return interactive(o, convo, text, cfg)
|
|
775
839
|
}
|
|
776
840
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
841
|
+
// Run as the `aipictury` command; required (a check), it only hands out what can be tested alone.
|
|
842
|
+
if (require.main === module) {
|
|
843
|
+
main().then((code) => {
|
|
844
|
+
require(path.join(EXT, 'agent.js')).stopBackground?.()
|
|
845
|
+
try { require(path.join(EXT, 'mcp.js')).closeAll() } catch { /* none */ }
|
|
846
|
+
require(path.join(EXT, 'browser.js')).close().finally(() => process.exit(code))
|
|
847
|
+
}, (error) => { process.stderr.write(`aipictury: ${error.stack || error.message}\n`); process.exit(1) })
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
module.exports = { approvalText, refusedText }
|
package/package.json
CHANGED
package/vscode-shim.js
CHANGED
|
@@ -242,6 +242,9 @@ module.exports = {
|
|
|
242
242
|
window: {
|
|
243
243
|
activeTextEditor: undefined,
|
|
244
244
|
visibleTextEditors: [],
|
|
245
|
+
// A terminal cannot tell whether it is in front: taken as not, so push_notification is sent
|
|
246
|
+
// (1.20.0: without state, its TypeError ended the CLI's run).
|
|
247
|
+
state: { focused: false },
|
|
245
248
|
createOutputChannel: outputChannel,
|
|
246
249
|
showInformationMessage: async () => undefined,
|
|
247
250
|
showWarningMessage: async () => undefined,
|