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.
Files changed (40) hide show
  1. package/dist/agent-failures.js +4 -4
  2. package/dist/agent-prompts.d.ts +9 -1
  3. package/dist/agent-prompts.js +15 -2
  4. package/dist/agent.js +125 -40
  5. package/dist/deckLocatorShape.d.ts +16 -0
  6. package/dist/deckLocatorShape.js +28 -0
  7. package/dist/editorConfig.d.ts +1 -0
  8. package/dist/shell/assets/index-C5DS_7UM.js +447 -0
  9. package/dist/shell/assets/index-CoU3ETYM.css +1 -0
  10. package/dist/shell/index.html +2 -2
  11. package/kits/base/CLAUDE.md +3 -0
  12. package/kits/base/castle.json +21 -11
  13. package/kits/physics-2d/CLAUDE.md +41 -8
  14. package/kits/physics-2d/behaviors/AnalogStick.jsx +80 -4
  15. package/kits/physics-2d/behaviors/Slingshot.jsx +14 -2
  16. package/kits/physics-2d/behaviors/Sprite.jsx +17 -8
  17. package/kits/physics-2d/behaviors/Style.jsx +270 -0
  18. package/kits/physics-2d/behaviors/Text.jsx +213 -0
  19. package/kits/physics-2d/behaviors/Video.jsx +8 -4
  20. package/kits/physics-2d/blueprints/text.scene +12 -0
  21. package/kits/physics-2d/castle.json +10 -6
  22. package/kits/physics-2d/editors/SceneEditor.jsx +41 -0
  23. package/kits/physics-2d/editors/deckFont.js +65 -55
  24. package/kits/physics-2d/editors/fontPreview.js +6 -38
  25. package/kits/physics-2d/editors/pixelInspector.jsx +4 -154
  26. package/kits/physics-2d/engine/blueprint.js +28 -0
  27. package/kits/physics-2d/engine/fonts.js +125 -23
  28. package/kits/physics-2d/engine/paletteField.jsx +235 -0
  29. package/kits/physics-2d/engine/physics/controls.js +5 -81
  30. package/kits/physics-2d/engine/popoverDismiss.js +17 -0
  31. package/kits/physics-2d/engine/scene.js +12 -3
  32. package/kits/physics-2d/engine/spriteField.jsx +2 -15
  33. package/kits/physics-2d/engine/tap.js +90 -0
  34. package/kits/physics-2d/engine/text.js +94 -0
  35. package/kits/physics-2d/engine/ui.jsx +20 -0
  36. package/kits/physics-2d/engine/ui.module.css +10 -2
  37. package/kits/physics-3d/castle.json +4 -2
  38. package/package.json +1 -1
  39. package/dist/shell/assets/index-BWOEraUy.js +0 -447
  40. package/dist/shell/assets/index-BkVF1OXc.css +0 -1
@@ -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 Castle AI credits for this request — get more at castle.xyz/credits${tasksNote}`;
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 Castle AI credits — get more at castle.xyz/credits${tasksNote}`;
212
+ return `Out of Operator credits — get more at castle.xyz/credits${tasksNote}`;
213
213
  }
214
214
  if (opts.failure.castleSpendLimit) {
215
- return `Castle AI spending limit reached — check your balance at castle.xyz/credits${tasksNote}`;
215
+ return `Operator spending limit reached — check your balance at castle.xyz/credits${tasksNote}`;
216
216
  }
217
- return `Daily Castle AI limit reached${resetsClause(opts.failure.resetAtMs)}. Runs on your own API key or login aren't limited.${tasksNote}`;
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":
@@ -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
- files?: string[];
107
+ refs?: PromptRef[];
100
108
  }): string;
101
109
  export declare function buildTaskPrompt(opts: {
102
110
  deckLabel: string;
@@ -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.files && opts.files.length > 0) {
895
- parts.push(`The user pointed at these deck files: ${opts.files.join(", ")}. Read them with your read tool if you need them to reply. When you spawn a task one of these files concerns, name that path in that task's prompt so the agent reads it; do not mention it to tasks it does not concern.`);
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 files are path references, so the only cost is instruction bytes
642
- // -- but a client is not the one to decide how many the router is told about.
643
- const MAX_MESSAGE_FILES = 12;
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 Castle AI credits — get more at castle.xyz/credits',
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 Castle AI limit reached',
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
- // Deck-relative paths a client @-mentioned, keeping only the ones that name a
3051
- // real place in this deck. Nothing is read here: the paths travel as references
3052
- // and whoever needs the contents opens them.
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
- return kept;
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 createTaskFeeds(broadcast) {
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 > 80)
3172
- feed.splice(0, feed.length - 80);
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 path earns its
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 no files at all, which is a send worth
3974
+ // predates @-mentions wrote none at all, which is a send worth
3887
3975
  // recovering for its words, not a malformed one.
3888
- files: validMessageFiles(deckDir, item.files),
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 filePaths = [];
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.files.length > 0)
3996
- message.files = item.files;
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
- for (const file of item.files) {
4005
- if (!filePaths.includes(file))
4006
- filePaths.push(file);
4007
- }
4092
+ refs.push(...item.refs);
4008
4093
  }
4009
- return { texts, attachmentPaths, filePaths };
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, filePaths } = commitDrainedSends(drained, ctx.log);
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
- files: filePaths,
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, files, clientId) {
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
- files: validMessageFiles(ctx.deckDir, files),
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, files, clientId) => handleQueueUserMessage(ctx, text, images, files, clientId),
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.map.delete(task.id),
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.files, clientId ?? undefined);
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
+ }
@@ -17,6 +17,7 @@ export interface FileTypeConfig {
17
17
  editor?: string;
18
18
  data?: boolean | DataMode;
19
19
  unsupported?: string;
20
+ width?: number;
20
21
  }
21
22
  export type DataMode = 'json' | 'text';
22
23
  export declare function dataModeOf(type: {