castle-web-cli 0.4.177 → 0.4.179
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-failures.js +4 -4
- package/dist/agent-prompts.d.ts +9 -1
- package/dist/agent-prompts.js +15 -2
- package/dist/agent.js +125 -40
- package/dist/deckLocatorShape.d.ts +16 -0
- package/dist/deckLocatorShape.js +28 -0
- package/dist/editorConfig.d.ts +1 -0
- package/dist/shell/assets/index-C5DS_7UM.js +447 -0
- package/dist/shell/assets/index-CoU3ETYM.css +1 -0
- package/dist/shell/index.html +2 -2
- package/kits/base/CLAUDE.md +3 -0
- package/kits/base/castle.json +21 -11
- package/kits/physics-2d/CLAUDE.md +41 -8
- package/kits/physics-2d/behaviors/AnalogStick.jsx +80 -4
- package/kits/physics-2d/behaviors/Slingshot.jsx +14 -2
- package/kits/physics-2d/behaviors/Sprite.jsx +17 -8
- package/kits/physics-2d/behaviors/Style.jsx +270 -0
- package/kits/physics-2d/behaviors/Text.jsx +213 -0
- package/kits/physics-2d/behaviors/Video.jsx +8 -4
- package/kits/physics-2d/blueprints/text.scene +12 -0
- package/kits/physics-2d/castle.json +10 -6
- package/kits/physics-2d/editors/SceneEditor.jsx +41 -0
- package/kits/physics-2d/editors/deckFont.js +65 -55
- package/kits/physics-2d/editors/fontPreview.js +6 -38
- package/kits/physics-2d/editors/pixelInspector.jsx +4 -154
- package/kits/physics-2d/engine/blueprint.js +28 -0
- package/kits/physics-2d/engine/fonts.js +125 -23
- package/kits/physics-2d/engine/paletteField.jsx +235 -0
- package/kits/physics-2d/engine/physics/controls.js +5 -81
- package/kits/physics-2d/engine/popoverDismiss.js +17 -0
- package/kits/physics-2d/engine/scene.js +12 -3
- package/kits/physics-2d/engine/spriteField.jsx +2 -15
- package/kits/physics-2d/engine/tap.js +90 -0
- package/kits/physics-2d/engine/text.js +94 -0
- package/kits/physics-2d/engine/ui.jsx +20 -0
- package/kits/physics-2d/engine/ui.module.css +10 -2
- package/kits/physics-3d/castle.json +4 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-BWOEraUy.js +0 -447
- package/dist/shell/assets/index-BkVF1OXc.css +0 -1
package/dist/agent-failures.js
CHANGED
|
@@ -206,15 +206,15 @@ export function failureCopy(opts) {
|
|
|
206
206
|
return `${configCopy(opts.failure)}${tasksNote}`;
|
|
207
207
|
case "limit":
|
|
208
208
|
if (opts.failure.castleCreditsInsufficient) {
|
|
209
|
-
return `Not enough
|
|
209
|
+
return `Not enough Operator credits for this request — get more at castle.xyz/credits${tasksNote}`;
|
|
210
210
|
}
|
|
211
211
|
if (opts.failure.castleCreditsExhausted) {
|
|
212
|
-
return `Out of
|
|
212
|
+
return `Out of Operator credits — get more at castle.xyz/credits${tasksNote}`;
|
|
213
213
|
}
|
|
214
214
|
if (opts.failure.castleSpendLimit) {
|
|
215
|
-
return `
|
|
215
|
+
return `Operator spending limit reached — check your balance at castle.xyz/credits${tasksNote}`;
|
|
216
216
|
}
|
|
217
|
-
return `Daily
|
|
217
|
+
return `Daily Operator limit reached${resetsClause(opts.failure.resetAtMs)}. Runs on your own API key or login aren't limited.${tasksNote}`;
|
|
218
218
|
case "transient":
|
|
219
219
|
return `OpenRouter is busy right now and I couldn't get through. Send that again in a moment.${tasksNote}`;
|
|
220
220
|
case "no-work":
|
package/dist/agent-prompts.d.ts
CHANGED
|
@@ -92,11 +92,19 @@ export interface RouterPromptOpts {
|
|
|
92
92
|
}
|
|
93
93
|
export declare function buildRouterPromptParts(opts: RouterPromptOpts): RouterPromptParts;
|
|
94
94
|
export declare function buildRouterPrompt(opts: RouterPromptOpts): string;
|
|
95
|
+
export interface PromptRef {
|
|
96
|
+
path: string;
|
|
97
|
+
line?: number;
|
|
98
|
+
endLine?: number;
|
|
99
|
+
view?: "params";
|
|
100
|
+
field?: string;
|
|
101
|
+
fragment?: string;
|
|
102
|
+
}
|
|
95
103
|
export declare function userTurnInstruction(opts: {
|
|
96
104
|
messages: string[];
|
|
97
105
|
interruptedDraft?: string;
|
|
98
106
|
attachments?: string[];
|
|
99
|
-
|
|
107
|
+
refs?: PromptRef[];
|
|
100
108
|
}): string;
|
|
101
109
|
export declare function buildTaskPrompt(opts: {
|
|
102
110
|
deckLabel: string;
|
package/dist/agent-prompts.js
CHANGED
|
@@ -871,6 +871,19 @@ export function buildRouterPrompt(opts) {
|
|
|
871
871
|
const parts = buildRouterPromptParts(opts);
|
|
872
872
|
return `${parts.system}\n\n${parts.user}`;
|
|
873
873
|
}
|
|
874
|
+
function refPhrase(ref) {
|
|
875
|
+
if (ref.line !== undefined) {
|
|
876
|
+
return ref.endLine !== undefined
|
|
877
|
+
? `lines ${ref.line} to ${ref.endLine} of ${ref.path}`
|
|
878
|
+
: `line ${ref.line} of ${ref.path}`;
|
|
879
|
+
}
|
|
880
|
+
if (ref.view === "params") {
|
|
881
|
+
return ref.field
|
|
882
|
+
? `the ${ref.field} param in ${ref.path}'s PARAMS`
|
|
883
|
+
: `the PARAMS of ${ref.path}`;
|
|
884
|
+
}
|
|
885
|
+
return ref.fragment ? `${ref.path} (at ${ref.fragment})` : ref.path;
|
|
886
|
+
}
|
|
874
887
|
export function userTurnInstruction(opts) {
|
|
875
888
|
const parts = [];
|
|
876
889
|
if (opts.interruptedDraft?.trim()) {
|
|
@@ -891,8 +904,8 @@ export function userTurnInstruction(opts) {
|
|
|
891
904
|
// while the CLI backends open the saved files themselves.
|
|
892
905
|
parts.push(`The user attached image file(s), saved in the deck at: ${opts.attachments.join(", ")}. If the images are not already visible in this message, open them with your read tool. Take them into account; pass the paths along to task agents that need them.`);
|
|
893
906
|
}
|
|
894
|
-
if (opts.
|
|
895
|
-
parts.push(`The user pointed at
|
|
907
|
+
if (opts.refs && opts.refs.length > 0) {
|
|
908
|
+
parts.push(`The user pointed at: ${opts.refs.map(refPhrase).join(", ")}. Read them with your read tool if you need them to reply. When you spawn a task one of these concerns, name it the same way in that task's prompt -- the path and the lines, or the path and the param -- so the agent reads that place; do not mention it to tasks it does not concern.`);
|
|
896
909
|
}
|
|
897
910
|
return parts.join("\n\n");
|
|
898
911
|
}
|
package/dist/agent.js
CHANGED
|
@@ -19,6 +19,7 @@ import { quickReferenceFor } from './platformDoc.js';
|
|
|
19
19
|
import { IMPORTS_DIR } from './imports.js';
|
|
20
20
|
import { isSameOriginUpgrade } from './wsOrigin.js';
|
|
21
21
|
import { resolveDeckPath } from './ide.js';
|
|
22
|
+
import { lineRange, uniqueLocators } from './deckLocatorShape.js';
|
|
22
23
|
import * as fs from 'fs';
|
|
23
24
|
import * as os from 'os';
|
|
24
25
|
import * as path from 'path';
|
|
@@ -638,9 +639,9 @@ const RESULT_SUMMARY_CHARS = 600;
|
|
|
638
639
|
// TRANSCRIPT_BYTE_BUDGET in agent-prompts.ts), so this term needs its own bound.
|
|
639
640
|
const PLAN_BYTE_BUDGET = 8 * 1024;
|
|
640
641
|
const MAX_ATTACHMENTS = 6;
|
|
641
|
-
// @-mentioned
|
|
642
|
-
//
|
|
643
|
-
const
|
|
642
|
+
// @-mentioned places are references, so the only cost is instruction bytes --
|
|
643
|
+
// but a client is not the one to decide how many the router is told about.
|
|
644
|
+
const MAX_MESSAGE_REFS = 12;
|
|
644
645
|
const MAX_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
645
646
|
const TERMINAL_STATUSES = ['done', 'failed', 'interrupted'];
|
|
646
647
|
function nowIso() {
|
|
@@ -2040,13 +2041,13 @@ async function castleSpendRefusal(backend, claudeModel, orAuth) {
|
|
|
2040
2041
|
if (castleCreditsExhausted(budget, credits)) {
|
|
2041
2042
|
return {
|
|
2042
2043
|
kind: 'limit',
|
|
2043
|
-
detail: 'Out of
|
|
2044
|
+
detail: 'Out of Operator credits — get more at castle.xyz/credits',
|
|
2044
2045
|
castleCreditsExhausted: true,
|
|
2045
2046
|
};
|
|
2046
2047
|
}
|
|
2047
2048
|
return {
|
|
2048
2049
|
kind: 'limit',
|
|
2049
|
-
detail: 'daily
|
|
2050
|
+
detail: 'daily Operator limit reached',
|
|
2050
2051
|
resetAtMs: budget.resetAtMs || undefined,
|
|
2051
2052
|
};
|
|
2052
2053
|
}
|
|
@@ -2268,6 +2269,8 @@ function persistTaskFile(tasksDir, task) {
|
|
|
2268
2269
|
// written whether or not the deck has the plan doc switched on: it is a disk
|
|
2269
2270
|
// receipt, never injected into a prompt, so it costs an off deck nothing.
|
|
2270
2271
|
const TASK_INDEX_FILE = 'index.md';
|
|
2272
|
+
const TASK_FEED_FILE = 'feed.json';
|
|
2273
|
+
const TASK_FEED_CAP = 80;
|
|
2271
2274
|
const TASK_INDEX_HEADER = '# tasks\n# <task-id> | <item> | <title> | <created> | <status> | <touching>\n';
|
|
2272
2275
|
// A pipe-delimited row can't carry a pipe. Titles come from the router's fence
|
|
2273
2276
|
// and can hold anything.
|
|
@@ -3047,19 +3050,85 @@ function saveAttachments(attachmentsDir, messageId, images) {
|
|
|
3047
3050
|
}
|
|
3048
3051
|
return saved;
|
|
3049
3052
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
function validMessageFiles(deckDir, files) {
|
|
3054
|
-
if (!Array.isArray(files))
|
|
3055
|
-
return [];
|
|
3056
|
-
const kept = [];
|
|
3057
|
-
for (const entry of files.slice(0, MAX_MESSAGE_FILES)) {
|
|
3058
|
-
const resolved = resolveDeckPath(deckDir, entry);
|
|
3059
|
-
if (resolved.ok && !kept.includes(resolved.rel))
|
|
3060
|
-
kept.push(resolved.rel);
|
|
3053
|
+
function isExistingFile(abs) {
|
|
3054
|
+
try {
|
|
3055
|
+
return fs.statSync(abs).isFile();
|
|
3061
3056
|
}
|
|
3062
|
-
|
|
3057
|
+
catch {
|
|
3058
|
+
return false;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
// Past this a line number goes through unclamped, rather than reading a file
|
|
3062
|
+
// that size to count lines nobody hand-edits by number.
|
|
3063
|
+
const MAX_CLAMP_BYTES = 2 * 1024 * 1024;
|
|
3064
|
+
function lineCountOf(abs) {
|
|
3065
|
+
try {
|
|
3066
|
+
if (fs.statSync(abs).size > MAX_CLAMP_BYTES)
|
|
3067
|
+
return null;
|
|
3068
|
+
const bytes = fs.readFileSync(abs);
|
|
3069
|
+
let lines = 1;
|
|
3070
|
+
for (let i = bytes.indexOf(10); i !== -1; i = bytes.indexOf(10, i + 1))
|
|
3071
|
+
lines++;
|
|
3072
|
+
return lines;
|
|
3073
|
+
}
|
|
3074
|
+
catch {
|
|
3075
|
+
return null;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
function isPositiveInteger(value) {
|
|
3079
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0;
|
|
3080
|
+
}
|
|
3081
|
+
// A `ParamSlot.id` (shell/paramsFile.ts): `speed`, or `player.speed` in a group.
|
|
3082
|
+
const PARAM_FIELD_RE = /^[\w$]+(?:\.[\w$]+)*$/;
|
|
3083
|
+
const MAX_FRAGMENT_CHARS = 200;
|
|
3084
|
+
// One place a client @-mentioned, or null when it names no file this deck has.
|
|
3085
|
+
// A part that does not hold up -- a line that is not a positive integer, a field
|
|
3086
|
+
// that is not a field name -- is dropped and the file still stands. Whether a
|
|
3087
|
+
// params field exists is not checked: that means parsing the file, and the
|
|
3088
|
+
// router reads it anyway.
|
|
3089
|
+
function validMessageRef(deckDir, entry, lineCounts) {
|
|
3090
|
+
if (!entry || typeof entry !== 'object')
|
|
3091
|
+
return null;
|
|
3092
|
+
const raw = entry;
|
|
3093
|
+
const resolved = resolveDeckPath(deckDir, raw.path);
|
|
3094
|
+
if (!resolved.ok || !isExistingFile(resolved.abs))
|
|
3095
|
+
return null;
|
|
3096
|
+
const path = resolved.rel;
|
|
3097
|
+
if (isPositiveInteger(raw.line)) {
|
|
3098
|
+
if (!lineCounts.has(path))
|
|
3099
|
+
lineCounts.set(path, lineCountOf(resolved.abs));
|
|
3100
|
+
const lines = lineCounts.get(path) ?? null;
|
|
3101
|
+
const clamp = (n) => (lines === null ? n : Math.min(n, lines));
|
|
3102
|
+
const end = isPositiveInteger(raw.endLine) ? clamp(raw.endLine) : undefined;
|
|
3103
|
+
return { path, ...lineRange(clamp(raw.line), end) };
|
|
3104
|
+
}
|
|
3105
|
+
if (raw.view === 'params') {
|
|
3106
|
+
const field = typeof raw.field === 'string' && PARAM_FIELD_RE.test(raw.field) ? raw.field : null;
|
|
3107
|
+
return field ? { path, view: 'params', field } : { path, view: 'params' };
|
|
3108
|
+
}
|
|
3109
|
+
const fragment = typeof raw.fragment === 'string' ? raw.fragment : '';
|
|
3110
|
+
if (fragment && fragment.length <= MAX_FRAGMENT_CHARS)
|
|
3111
|
+
return { path, fragment };
|
|
3112
|
+
return { path };
|
|
3113
|
+
}
|
|
3114
|
+
// The places a client @-mentioned, keeping only the ones that name a real place
|
|
3115
|
+
// in this deck. Nothing is read here but a line count: the refs travel as
|
|
3116
|
+
// references and whoever needs the contents opens them.
|
|
3117
|
+
function validMessageRefs(deckDir, refs) {
|
|
3118
|
+
if (!Array.isArray(refs))
|
|
3119
|
+
return [];
|
|
3120
|
+
const lineCounts = new Map();
|
|
3121
|
+
const valid = refs
|
|
3122
|
+
.slice(0, MAX_MESSAGE_REFS)
|
|
3123
|
+
.map((entry) => validMessageRef(deckDir, entry, lineCounts))
|
|
3124
|
+
.filter((ref) => ref !== null);
|
|
3125
|
+
return uniqueLocators(valid);
|
|
3126
|
+
}
|
|
3127
|
+
// A client, or a queue mirror, from before refs replaced `files` sends bare paths.
|
|
3128
|
+
function refsOrLegacyFiles(carrier) {
|
|
3129
|
+
if (carrier.refs !== undefined || !Array.isArray(carrier.files))
|
|
3130
|
+
return carrier.refs;
|
|
3131
|
+
return carrier.files.map((path) => ({ path }));
|
|
3063
3132
|
}
|
|
3064
3133
|
// Cap on the failure detail shown in a board row -- just enough for the
|
|
3065
3134
|
// router to reason about what went wrong, not the full crash dump.
|
|
@@ -3157,8 +3226,27 @@ function asClientMessage(message, signPath) {
|
|
|
3157
3226
|
attachments: message.attachments.map((name) => clientUrl(`${AGENT_ATTACHMENT_PREFIX}${name}`, signPath)),
|
|
3158
3227
|
};
|
|
3159
3228
|
}
|
|
3160
|
-
function
|
|
3229
|
+
function loadTaskFeeds(tasksDir) {
|
|
3161
3230
|
const map = new Map();
|
|
3231
|
+
for (const entry of fs.existsSync(tasksDir) ? fs.readdirSync(tasksDir) : []) {
|
|
3232
|
+
const loaded = readJsonFile(path.join(tasksDir, entry, TASK_FEED_FILE));
|
|
3233
|
+
if (!Array.isArray(loaded))
|
|
3234
|
+
continue;
|
|
3235
|
+
const lines = loaded.filter((line) => typeof line === 'string');
|
|
3236
|
+
if (lines.length === 0)
|
|
3237
|
+
continue;
|
|
3238
|
+
map.set(entry, lines.slice(-TASK_FEED_CAP));
|
|
3239
|
+
}
|
|
3240
|
+
return map;
|
|
3241
|
+
}
|
|
3242
|
+
function createTaskFeeds(broadcast, tasksDir) {
|
|
3243
|
+
const map = loadTaskFeeds(tasksDir);
|
|
3244
|
+
function persist(task) {
|
|
3245
|
+
const feed = map.get(task.id);
|
|
3246
|
+
if (!feed || feed.length === 0)
|
|
3247
|
+
return;
|
|
3248
|
+
atomicWriteFileSync(path.join(tasksDir, task.id, TASK_FEED_FILE), JSON.stringify(feed, null, 2) + '\n');
|
|
3249
|
+
}
|
|
3162
3250
|
function push(task, entry) {
|
|
3163
3251
|
let feed = map.get(task.id);
|
|
3164
3252
|
if (!feed) {
|
|
@@ -3168,11 +3256,11 @@ function createTaskFeeds(broadcast) {
|
|
|
3168
3256
|
if (feed[feed.length - 1] === entry)
|
|
3169
3257
|
return;
|
|
3170
3258
|
feed.push(entry);
|
|
3171
|
-
if (feed.length >
|
|
3172
|
-
feed.splice(0, feed.length -
|
|
3259
|
+
if (feed.length > TASK_FEED_CAP)
|
|
3260
|
+
feed.splice(0, feed.length - TASK_FEED_CAP);
|
|
3173
3261
|
broadcast({ type: 'task-feed', id: task.id, entry });
|
|
3174
3262
|
}
|
|
3175
|
-
return { map, push };
|
|
3263
|
+
return { map, push, persist };
|
|
3176
3264
|
}
|
|
3177
3265
|
function createMessageThinking(broadcast) {
|
|
3178
3266
|
const map = new Map();
|
|
@@ -3881,11 +3969,11 @@ function loadRecoverableSends(deckDir, pendingPath, committedIds) {
|
|
|
3881
3969
|
id: item.id,
|
|
3882
3970
|
text: item.text,
|
|
3883
3971
|
attachments: item.attachments.filter((a) => typeof a === 'string'),
|
|
3884
|
-
// Re-validated rather than trusted: enqueue is where a
|
|
3972
|
+
// Re-validated rather than trusted: enqueue is where a ref earns its
|
|
3885
3973
|
// place, and a mirror on disk is not evidence it ever did. A build that
|
|
3886
|
-
// predates @-mentions wrote
|
|
3974
|
+
// predates @-mentions wrote none at all, which is a send worth
|
|
3887
3975
|
// recovering for its words, not a malformed one.
|
|
3888
|
-
|
|
3976
|
+
refs: validMessageRefs(deckDir, refsOrLegacyFiles(item)),
|
|
3889
3977
|
});
|
|
3890
3978
|
}
|
|
3891
3979
|
}
|
|
@@ -3980,7 +4068,7 @@ function startRouterTurn(ctx, instruction, attachments = []) {
|
|
|
3980
4068
|
function commitDrainedSends(drained, log) {
|
|
3981
4069
|
const texts = [];
|
|
3982
4070
|
const attachmentPaths = [];
|
|
3983
|
-
const
|
|
4071
|
+
const refs = [];
|
|
3984
4072
|
for (const item of drained) {
|
|
3985
4073
|
if (!item.logged) {
|
|
3986
4074
|
const message = {
|
|
@@ -3992,8 +4080,8 @@ function commitDrainedSends(drained, log) {
|
|
|
3992
4080
|
};
|
|
3993
4081
|
if (item.attachments.length > 0)
|
|
3994
4082
|
message.attachments = item.attachments;
|
|
3995
|
-
if (item.
|
|
3996
|
-
message.
|
|
4083
|
+
if (item.refs.length > 0)
|
|
4084
|
+
message.refs = item.refs;
|
|
3997
4085
|
log.add(message);
|
|
3998
4086
|
}
|
|
3999
4087
|
if (item.text.trim())
|
|
@@ -4001,12 +4089,9 @@ function commitDrainedSends(drained, log) {
|
|
|
4001
4089
|
for (const name of item.attachments) {
|
|
4002
4090
|
attachmentPaths.push(path.join('.castle', 'agent', 'attachments', name));
|
|
4003
4091
|
}
|
|
4004
|
-
|
|
4005
|
-
if (!filePaths.includes(file))
|
|
4006
|
-
filePaths.push(file);
|
|
4007
|
-
}
|
|
4092
|
+
refs.push(...item.refs);
|
|
4008
4093
|
}
|
|
4009
|
-
return { texts, attachmentPaths,
|
|
4094
|
+
return { texts, attachmentPaths, refs: uniqueLocators(refs) };
|
|
4010
4095
|
}
|
|
4011
4096
|
// Drain the queue into the log as real user messages and start one follow-up
|
|
4012
4097
|
// turn addressing them all (a burst batches into a single turn). A pending
|
|
@@ -4022,7 +4107,7 @@ function maybeStartRouterQueueTurn(ctx) {
|
|
|
4022
4107
|
// Kept so a fold or manual interrupt of THIS turn can hand these sends back
|
|
4023
4108
|
// to reclaimInFlightSends if it dies before producing anything visible.
|
|
4024
4109
|
state.inFlightSends = drained;
|
|
4025
|
-
const { texts, attachmentPaths,
|
|
4110
|
+
const { texts, attachmentPaths, refs } = commitDrainedSends(drained, ctx.log);
|
|
4026
4111
|
// The queue is now committed to messages.json; clear its durable mirror.
|
|
4027
4112
|
persistPendingSends(ctx);
|
|
4028
4113
|
const draft = state.pendingInterruptedDraft;
|
|
@@ -4033,7 +4118,7 @@ function maybeStartRouterQueueTurn(ctx) {
|
|
|
4033
4118
|
messages: texts,
|
|
4034
4119
|
interruptedDraft: draft || undefined,
|
|
4035
4120
|
attachments: attachmentPaths,
|
|
4036
|
-
|
|
4121
|
+
refs,
|
|
4037
4122
|
}), attachmentPaths);
|
|
4038
4123
|
}
|
|
4039
4124
|
// The turn settled: clear the busy flag, broadcast it, then flush anything
|
|
@@ -4082,7 +4167,7 @@ function safeClientMessageId(clientId) {
|
|
|
4082
4167
|
return null;
|
|
4083
4168
|
return /^[A-Za-z0-9_-]{1,32}$/.test(clientId) ? clientId : null;
|
|
4084
4169
|
}
|
|
4085
|
-
function handleQueueUserMessage(ctx, text, images,
|
|
4170
|
+
function handleQueueUserMessage(ctx, text, images, refs, clientId) {
|
|
4086
4171
|
const { state } = ctx;
|
|
4087
4172
|
// Mid-run: queue (don't interrupt). It shows as a queued row in the
|
|
4088
4173
|
// composer and flushes when the current turn settles. Idle: start now.
|
|
@@ -4109,7 +4194,7 @@ function handleQueueUserMessage(ctx, text, images, files, clientId) {
|
|
|
4109
4194
|
id,
|
|
4110
4195
|
text,
|
|
4111
4196
|
attachments,
|
|
4112
|
-
|
|
4197
|
+
refs: validMessageRefs(ctx.deckDir, refs),
|
|
4113
4198
|
});
|
|
4114
4199
|
persistPendingSends(ctx);
|
|
4115
4200
|
if (state.routerRunning) {
|
|
@@ -4195,7 +4280,7 @@ function createRouterQueue(deps) {
|
|
|
4195
4280
|
persistPendingSends(ctx);
|
|
4196
4281
|
maybeStartRouterQueueTurn(ctx);
|
|
4197
4282
|
return {
|
|
4198
|
-
handleUserMessage: (text, images,
|
|
4283
|
+
handleUserMessage: (text, images, refs, clientId) => handleQueueUserMessage(ctx, text, images, refs, clientId),
|
|
4199
4284
|
interruptRouter: () => interruptRouterQueue(ctx),
|
|
4200
4285
|
cancelQueued: (index) => cancelQueuedSend(ctx, index),
|
|
4201
4286
|
isRunning: () => ctx.state.routerRunning,
|
|
@@ -4335,7 +4420,7 @@ export function createAgentServer(opts) {
|
|
|
4335
4420
|
// Castle's), so the bar follows the change instead of the poll.
|
|
4336
4421
|
usageFeed.refresh();
|
|
4337
4422
|
};
|
|
4338
|
-
const taskFeeds = createTaskFeeds(broadcast);
|
|
4423
|
+
const taskFeeds = createTaskFeeds(broadcast, tasksDir);
|
|
4339
4424
|
const messageThinking = createMessageThinking(broadcast);
|
|
4340
4425
|
const taskStore = createTaskStore({
|
|
4341
4426
|
deckDir,
|
|
@@ -4357,7 +4442,7 @@ export function createAgentServer(opts) {
|
|
|
4357
4442
|
onUpdate: (task) => broadcast({ type: 'task-update', task: asClientTask(task, opts.signPath) }),
|
|
4358
4443
|
onStarted: () => undefined,
|
|
4359
4444
|
onRetry: (task, attempt) => addLog(`agent died, retrying (${attempt}/${MAX_TASK_ATTEMPTS}): ${task.title}`),
|
|
4360
|
-
onFinished: (task) => taskFeeds.
|
|
4445
|
+
onFinished: (task) => taskFeeds.persist(task),
|
|
4361
4446
|
onFeed: (task, entry) => taskFeeds.push(task, entry),
|
|
4362
4447
|
});
|
|
4363
4448
|
// The mid-run send queue (queue-by-default, "send now"/Stop interrupt, epoch
|
|
@@ -4474,7 +4559,7 @@ export function createAgentServer(opts) {
|
|
|
4474
4559
|
if (!duplicate) {
|
|
4475
4560
|
if (clientId !== null)
|
|
4476
4561
|
rememberClientId(clientId);
|
|
4477
|
-
routerQueue.handleUserMessage(typeof msg.text === 'string' ? msg.text.trim() : '', msg.images, msg
|
|
4562
|
+
routerQueue.handleUserMessage(typeof msg.text === 'string' ? msg.text.trim() : '', msg.images, refsOrLegacyFiles(msg), clientId ?? undefined);
|
|
4478
4563
|
}
|
|
4479
4564
|
if (clientId !== null) {
|
|
4480
4565
|
if (socket.readyState === socket.OPEN) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface DeckLocator {
|
|
2
|
+
path: string;
|
|
3
|
+
line?: number;
|
|
4
|
+
/** Inclusive; only with `line`, and always greater than it. */
|
|
5
|
+
endLine?: number;
|
|
6
|
+
view?: 'params';
|
|
7
|
+
field?: string;
|
|
8
|
+
/** The fragment as written, when it meant nothing to the shell. */
|
|
9
|
+
fragment?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function lineRange(line: number, endLine?: number): {
|
|
12
|
+
line: number;
|
|
13
|
+
endLine?: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function formatLocator(loc: DeckLocator): string;
|
|
16
|
+
export declare function uniqueLocators(locs: DeckLocator[]): DeckLocator[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// A place in the deck, as both ends of a link see it: the shell parses and
|
|
2
|
+
// follows one, the serve validates the ones a user's message points at. Apart
|
|
3
|
+
// from shell/deckLocator.ts so the serve can use it without the shell's
|
|
4
|
+
// browser modules.
|
|
5
|
+
export function lineRange(line, endLine) {
|
|
6
|
+
if (endLine === undefined || endLine === line)
|
|
7
|
+
return { line };
|
|
8
|
+
return endLine > line ? { line, endLine } : { line: endLine, endLine: line };
|
|
9
|
+
}
|
|
10
|
+
export function formatLocator(loc) {
|
|
11
|
+
if (loc.line !== undefined) {
|
|
12
|
+
const end = loc.endLine === undefined ? '' : `-${loc.endLine}`;
|
|
13
|
+
return `${loc.path}#L${loc.line}${end}`;
|
|
14
|
+
}
|
|
15
|
+
if (loc.view === 'params') {
|
|
16
|
+
return loc.field ? `${loc.path}#params:${loc.field}` : `${loc.path}#params`;
|
|
17
|
+
}
|
|
18
|
+
return loc.fragment ? `${loc.path}#${loc.fragment}` : loc.path;
|
|
19
|
+
}
|
|
20
|
+
export function uniqueLocators(locs) {
|
|
21
|
+
const byKey = new Map();
|
|
22
|
+
for (const loc of locs) {
|
|
23
|
+
const key = formatLocator(loc);
|
|
24
|
+
if (!byKey.has(key))
|
|
25
|
+
byKey.set(key, loc);
|
|
26
|
+
}
|
|
27
|
+
return [...byKey.values()];
|
|
28
|
+
}
|