@aragon-agent/cli 0.6.35 → 0.6.37
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/CHANGELOG.md +21 -0
- package/README.md +3173 -3071
- package/dist/agent/agent-mode.d.ts +20 -4
- package/dist/agent/agent-mode.js +20 -4
- package/dist/agent/controller.d.ts +38 -3
- package/dist/agent/controller.js +64 -4
- package/dist/agent/headless.js +31 -0
- package/dist/agent/reducer.d.ts +13 -2
- package/dist/agent/reducer.js +1 -0
- package/dist/cli.js +12 -0
- package/dist/commands/agents-command.d.ts +15 -0
- package/dist/commands/agents-command.js +27 -0
- package/dist/commands/builtins.js +131 -4
- package/dist/commands/perf.d.ts +11 -0
- package/dist/commands/perf.js +2 -1
- package/dist/commands/registry.d.ts +14 -0
- package/dist/config/app-paths.d.ts +15 -0
- package/dist/config/app-paths.js +33 -2
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +28 -2
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +8 -1
- package/dist/config/schema.d.ts +68 -0
- package/dist/config/schema.js +39 -0
- package/dist/config/store.js +12 -1
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +25 -0
- package/dist/exec/events.js +4 -0
- package/dist/exec/index.js +16 -1
- package/dist/exec/runner.js +10 -0
- package/dist/proc/limits.d.ts +16 -0
- package/dist/proc/limits.js +16 -0
- package/dist/proc/supervisor.d.ts +20 -1
- package/dist/proc/supervisor.js +42 -14
- package/dist/prompts/command.d.ts +34 -0
- package/dist/prompts/command.js +237 -0
- package/dist/prompts/copy.d.ts +89 -0
- package/dist/prompts/copy.js +131 -0
- package/dist/prompts/fill.d.ts +51 -0
- package/dist/prompts/fill.js +46 -0
- package/dist/prompts/limits.d.ts +53 -0
- package/dist/prompts/limits.js +53 -0
- package/dist/prompts/normalize.d.ts +53 -0
- package/dist/prompts/normalize.js +190 -0
- package/dist/prompts/resolve.d.ts +31 -0
- package/dist/prompts/resolve.js +60 -0
- package/dist/prompts/search.d.ts +58 -0
- package/dist/prompts/search.js +146 -0
- package/dist/prompts/store.d.ts +86 -0
- package/dist/prompts/store.js +453 -0
- package/dist/prompts/types.d.ts +204 -0
- package/dist/prompts/types.js +13 -0
- package/dist/session/checkpoint.d.ts +8 -1
- package/dist/session/checkpoint.js +6 -0
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +13 -1
- package/dist/session/startup-session.d.ts +7 -2
- package/dist/session/startup-session.js +5 -2
- package/dist/session/store.js +3 -0
- package/dist/session/validate-session.js +64 -1
- package/dist/team/limits.d.ts +25 -0
- package/dist/team/limits.js +26 -0
- package/dist/team/runtime.js +27 -2
- package/dist/team/settled.d.ts +57 -0
- package/dist/team/settled.js +87 -0
- package/dist/team/subagent.js +48 -2
- package/dist/team/timeline.d.ts +74 -0
- package/dist/team/timeline.js +101 -0
- package/dist/team/types.d.ts +24 -1
- package/dist/todo/acceptance.d.ts +120 -0
- package/dist/todo/acceptance.js +399 -0
- package/dist/todo/follow-through.d.ts +48 -23
- package/dist/todo/follow-through.js +110 -44
- package/dist/todo/limits.d.ts +62 -1
- package/dist/todo/limits.js +62 -1
- package/dist/todo/panel-rows.d.ts +29 -39
- package/dist/todo/panel-rows.js +37 -83
- package/dist/todo/plan-sections.d.ts +60 -0
- package/dist/todo/plan-sections.js +108 -0
- package/dist/todo/prompt.d.ts +17 -12
- package/dist/todo/prompt.js +59 -40
- package/dist/todo/row-window.d.ts +51 -0
- package/dist/todo/row-window.js +107 -0
- package/dist/todo/store.d.ts +139 -31
- package/dist/todo/store.js +271 -73
- package/dist/todo/todo-tool.d.ts +33 -8
- package/dist/todo/todo-tool.js +186 -30
- package/dist/todo/types.d.ts +58 -0
- package/dist/ui/App.js +310 -46
- package/dist/ui/Composer.d.ts +6 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +2 -2
- package/dist/ui/PromptInput.d.ts +21 -1
- package/dist/ui/PromptInput.js +21 -1
- package/dist/ui/ScrollbackShell.d.ts +19 -19
- package/dist/ui/ScrollbackShell.js +8 -1
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/TeamPanel.d.ts +26 -44
- package/dist/ui/TeamPanel.js +46 -135
- package/dist/ui/TodoPanel.d.ts +17 -14
- package/dist/ui/TodoPanel.js +146 -38
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/composer-input.d.ts +10 -0
- package/dist/ui/composer-input.js +57 -24
- package/dist/ui/entries/TodoCard.d.ts +26 -11
- package/dist/ui/entries/TodoCard.js +56 -15
- package/dist/ui/glyphs.d.ts +19 -0
- package/dist/ui/glyphs.js +4 -0
- package/dist/ui/layout/agents-board.d.ts +102 -0
- package/dist/ui/layout/agents-board.js +389 -0
- package/dist/ui/layout/cells.d.ts +61 -0
- package/dist/ui/layout/cells.js +156 -0
- package/dist/ui/layout/header-hints.d.ts +13 -3
- package/dist/ui/layout/header-hints.js +48 -14
- package/dist/ui/layout/status-detail-layout.js +9 -0
- package/dist/ui/layout/status-layout.d.ts +13 -0
- package/dist/ui/layout/team-row.d.ts +139 -0
- package/dist/ui/layout/team-row.js +246 -0
- package/dist/ui/layout/todo-layout.d.ts +38 -0
- package/dist/ui/layout/todo-layout.js +75 -0
- package/dist/ui/layout/virtual-window.js +23 -3
- package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
- package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
- package/dist/ui/overlays/HelpOverlay.js +17 -2
- package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
- package/dist/ui/overlays/PromptEditorPage.js +227 -0
- package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
- package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
- package/dist/ui/overlays/PromptPicker.d.ts +36 -0
- package/dist/ui/overlays/PromptPicker.js +129 -0
- package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
- package/dist/ui/overlays/PromptTextArea.js +141 -0
- package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
- package/dist/ui/overlays/prompt-library-state.js +335 -0
- package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
- package/dist/ui/overlays/prompt-picker-state.js +98 -0
- package/dist/ui/scrollback/band.js +14 -1
- package/dist/ui/team-status.d.ts +25 -0
- package/dist/ui/team-status.js +31 -3
- package/dist/ui/use-startup-notices.d.ts +35 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.js +9 -1
- package/package.json +2 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a prompt lands in the composer (quick-prompt-library D-6 / §4.6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* PURE, AND IT KNOWS NOTHING ABOUT THE DRAFT'S REAL SHAPE. It is handed the current
|
|
7
|
+
* buffer and caret, and it answers "what should be inserted, or what should the new
|
|
8
|
+
* buffer be". It deliberately does NOT return the resulting buffer: a draft may
|
|
9
|
+
* hold paste-collapse TOKENS (`[Pasted text #1 +8 lines]` plus a payload map), so
|
|
10
|
+
* "the buffer after applying this plan" is a fact only `editorReducer` can compute.
|
|
11
|
+
* Recomputing it here would be a second authority, and the two would drift exactly
|
|
12
|
+
* on the drafts this feature exists to append to.
|
|
13
|
+
*
|
|
14
|
+
* WHY `insert` IS THE DEFAULT (and why the decision lives here rather than in
|
|
15
|
+
* `App`): the only place that knows what the draft currently holds is the editor
|
|
16
|
+
* (the host has no copy of the text), and silently discarding what someone has
|
|
17
|
+
* already typed is the one thing a "fill" gesture must never do.
|
|
18
|
+
*/
|
|
19
|
+
export interface PromptFillInput {
|
|
20
|
+
/** The current draft buffer. */
|
|
21
|
+
draft: string;
|
|
22
|
+
/** Caret offset into `draft`, in UTF-16 units. */
|
|
23
|
+
cursor: number;
|
|
24
|
+
/** The prompt body. */
|
|
25
|
+
content: string;
|
|
26
|
+
/** `insert` unless the user configured `prompts.insertMode = 'replace'` (D-19). */
|
|
27
|
+
mode?: 'insert' | 'replace';
|
|
28
|
+
}
|
|
29
|
+
export interface PromptFillPlan {
|
|
30
|
+
mode: 'replace' | 'insert';
|
|
31
|
+
/**
|
|
32
|
+
* `replace`: the new buffer in full.
|
|
33
|
+
* `insert`: only the fragment to insert, separators included.
|
|
34
|
+
*/
|
|
35
|
+
text: string;
|
|
36
|
+
/**
|
|
37
|
+
* `replace`: where the caret goes afterwards (past the body).
|
|
38
|
+
* `insert`: ignored - the reducer owns the caret and the token snapping.
|
|
39
|
+
*/
|
|
40
|
+
cursor: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Decide between replacing and inserting, and build the fragment.
|
|
44
|
+
*
|
|
45
|
+
* A blank draft is replaced even in `insert` mode: "insert at the caret" into an
|
|
46
|
+
* empty buffer and "replace the empty buffer" are the same edit, and saying so
|
|
47
|
+
* keeps the caller from having to special-case it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function planPromptFill(input: PromptFillInput): PromptFillPlan;
|
|
50
|
+
/** Lines a body occupies, for the "8 lines, editable" acknowledgement. */
|
|
51
|
+
export declare function promptLineCount(content: string): number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a prompt lands in the composer (quick-prompt-library D-6 / §4.6).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* PURE, AND IT KNOWS NOTHING ABOUT THE DRAFT'S REAL SHAPE. It is handed the current
|
|
7
|
+
* buffer and caret, and it answers "what should be inserted, or what should the new
|
|
8
|
+
* buffer be". It deliberately does NOT return the resulting buffer: a draft may
|
|
9
|
+
* hold paste-collapse TOKENS (`[Pasted text #1 +8 lines]` plus a payload map), so
|
|
10
|
+
* "the buffer after applying this plan" is a fact only `editorReducer` can compute.
|
|
11
|
+
* Recomputing it here would be a second authority, and the two would drift exactly
|
|
12
|
+
* on the drafts this feature exists to append to.
|
|
13
|
+
*
|
|
14
|
+
* WHY `insert` IS THE DEFAULT (and why the decision lives here rather than in
|
|
15
|
+
* `App`): the only place that knows what the draft currently holds is the editor
|
|
16
|
+
* (the host has no copy of the text), and silently discarding what someone has
|
|
17
|
+
* already typed is the one thing a "fill" gesture must never do.
|
|
18
|
+
*/
|
|
19
|
+
/** Two newlines, so the typed text and the prompt read as two paragraphs. */
|
|
20
|
+
const SEPARATOR = '\n\n';
|
|
21
|
+
/**
|
|
22
|
+
* Decide between replacing and inserting, and build the fragment.
|
|
23
|
+
*
|
|
24
|
+
* A blank draft is replaced even in `insert` mode: "insert at the caret" into an
|
|
25
|
+
* empty buffer and "replace the empty buffer" are the same edit, and saying so
|
|
26
|
+
* keeps the caller from having to special-case it.
|
|
27
|
+
*/
|
|
28
|
+
export function planPromptFill(input) {
|
|
29
|
+
const cursor = Math.max(0, Math.min(input.draft.length, Math.floor(input.cursor)));
|
|
30
|
+
const blank = input.draft.trim().length === 0;
|
|
31
|
+
if (input.mode === 'replace' || blank) {
|
|
32
|
+
return { mode: 'replace', text: input.content, cursor: input.content.length };
|
|
33
|
+
}
|
|
34
|
+
const before = input.draft.slice(0, cursor);
|
|
35
|
+
const after = input.draft.slice(cursor);
|
|
36
|
+
// A SEPARATOR IS ADDED ONLY WHERE ONE IS MISSING, which is what keeps the plan
|
|
37
|
+
// from producing three newlines when the caret already sits on a blank line.
|
|
38
|
+
const lead = before.length > 0 && !before.endsWith(SEPARATOR) ? SEPARATOR : '';
|
|
39
|
+
const trail = after.length > 0 && !after.startsWith(SEPARATOR) ? SEPARATOR : '';
|
|
40
|
+
const text = `${lead}${input.content}${trail}`;
|
|
41
|
+
return { mode: 'insert', text, cursor: cursor + lead.length };
|
|
42
|
+
}
|
|
43
|
+
/** Lines a body occupies, for the "8 lines, editable" acknowledgement. */
|
|
44
|
+
export function promptLineCount(content) {
|
|
45
|
+
return content.split('\n').length;
|
|
46
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural bounds for the quick-prompt library (quick-prompt-library §5.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* STRUCTURE, NOT POLICY (index.md:52-56). Nothing here is user-tunable: these are
|
|
7
|
+
* what an entry, the library and the picker's query can physically carry. The
|
|
8
|
+
* three keys a user may actually change (`prompts.enabled`, `insertMode`, `hint`)
|
|
9
|
+
* live in `config/schema.ts`, are clamped there, and are persisted there. Mixing
|
|
10
|
+
* the two is how a "temporary" ceiling becomes a setting nobody clamps.
|
|
11
|
+
*
|
|
12
|
+
* EVERY NUMBER IS A CODE-POINT COUNT, not a byte count and not a UTF-16 length.
|
|
13
|
+
* Titles and bodies are typed in Chinese by the users this feature exists for, so
|
|
14
|
+
* a byte-based ceiling would silently allow a third of the characters a
|
|
15
|
+
* Latin-alphabet user gets. `[...text].length` is the measurement everywhere in
|
|
16
|
+
* this tree.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PROMPT_LIMITS: {
|
|
19
|
+
/** Entries one library may hold. The 201st create is REFUSED, never truncated. */
|
|
20
|
+
readonly maxPrompts: 200;
|
|
21
|
+
/** Title, without surrounding whitespace. */
|
|
22
|
+
readonly titleMaxChars: 80;
|
|
23
|
+
/** Body. Large enough for a 40-line procedure, small enough to rewrite cheaply. */
|
|
24
|
+
readonly contentMaxChars: 8000;
|
|
25
|
+
/** Optional one-line note under the title. */
|
|
26
|
+
readonly descriptionMaxChars: 200;
|
|
27
|
+
/** Tags per entry. */
|
|
28
|
+
readonly maxTags: 6;
|
|
29
|
+
/** Characters per tag. */
|
|
30
|
+
readonly tagMaxChars: 24;
|
|
31
|
+
/** Body preview lines the list page's detail block may spend (read-only). */
|
|
32
|
+
readonly detailPreviewLines: 3;
|
|
33
|
+
/** Rows the list page's detail block may occupy, preview lines included. */
|
|
34
|
+
readonly detailMaxLines: 6;
|
|
35
|
+
/** Characters of a body shown on ONE preview line. */
|
|
36
|
+
readonly previewLineChars: 72;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The picker's own structural bounds.
|
|
40
|
+
*
|
|
41
|
+
* A SEPARATE OBJECT because the picker is a separate surface with a separate
|
|
42
|
+
* budget: its rows carry an index and a title and nothing else, and its query is
|
|
43
|
+
* a filter rather than content. Sharing one object would tie a future change to
|
|
44
|
+
* the library's row height.
|
|
45
|
+
*/
|
|
46
|
+
export declare const PROMPT_PICKER_LIMITS: {
|
|
47
|
+
/** Longest query the filter accepts; further typing is ignored. */
|
|
48
|
+
readonly queryMaxChars: 64;
|
|
49
|
+
/** `1`-`9` pick directly, so nine. */
|
|
50
|
+
readonly quickDigits: 9;
|
|
51
|
+
/** Rows the picker's detail block may occupy. */
|
|
52
|
+
readonly detailMaxLines: 4;
|
|
53
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural bounds for the quick-prompt library (quick-prompt-library §5.1).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* STRUCTURE, NOT POLICY (index.md:52-56). Nothing here is user-tunable: these are
|
|
7
|
+
* what an entry, the library and the picker's query can physically carry. The
|
|
8
|
+
* three keys a user may actually change (`prompts.enabled`, `insertMode`, `hint`)
|
|
9
|
+
* live in `config/schema.ts`, are clamped there, and are persisted there. Mixing
|
|
10
|
+
* the two is how a "temporary" ceiling becomes a setting nobody clamps.
|
|
11
|
+
*
|
|
12
|
+
* EVERY NUMBER IS A CODE-POINT COUNT, not a byte count and not a UTF-16 length.
|
|
13
|
+
* Titles and bodies are typed in Chinese by the users this feature exists for, so
|
|
14
|
+
* a byte-based ceiling would silently allow a third of the characters a
|
|
15
|
+
* Latin-alphabet user gets. `[...text].length` is the measurement everywhere in
|
|
16
|
+
* this tree.
|
|
17
|
+
*/
|
|
18
|
+
export const PROMPT_LIMITS = {
|
|
19
|
+
/** Entries one library may hold. The 201st create is REFUSED, never truncated. */
|
|
20
|
+
maxPrompts: 200,
|
|
21
|
+
/** Title, without surrounding whitespace. */
|
|
22
|
+
titleMaxChars: 80,
|
|
23
|
+
/** Body. Large enough for a 40-line procedure, small enough to rewrite cheaply. */
|
|
24
|
+
contentMaxChars: 8000,
|
|
25
|
+
/** Optional one-line note under the title. */
|
|
26
|
+
descriptionMaxChars: 200,
|
|
27
|
+
/** Tags per entry. */
|
|
28
|
+
maxTags: 6,
|
|
29
|
+
/** Characters per tag. */
|
|
30
|
+
tagMaxChars: 24,
|
|
31
|
+
/** Body preview lines the list page's detail block may spend (read-only). */
|
|
32
|
+
detailPreviewLines: 3,
|
|
33
|
+
/** Rows the list page's detail block may occupy, preview lines included. */
|
|
34
|
+
detailMaxLines: 6,
|
|
35
|
+
/** Characters of a body shown on ONE preview line. */
|
|
36
|
+
previewLineChars: 72,
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The picker's own structural bounds.
|
|
40
|
+
*
|
|
41
|
+
* A SEPARATE OBJECT because the picker is a separate surface with a separate
|
|
42
|
+
* budget: its rows carry an index and a title and nothing else, and its query is
|
|
43
|
+
* a filter rather than content. Sharing one object would tie a future change to
|
|
44
|
+
* the library's row height.
|
|
45
|
+
*/
|
|
46
|
+
export const PROMPT_PICKER_LIMITS = {
|
|
47
|
+
/** Longest query the filter accepts; further typing is ignored. */
|
|
48
|
+
queryMaxChars: 64,
|
|
49
|
+
/** `1`-`9` pick directly, so nine. */
|
|
50
|
+
quickDigits: 9,
|
|
51
|
+
/** Rows the picker's detail block may occupy. */
|
|
52
|
+
detailMaxLines: 4,
|
|
53
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the library off disk without trusting it (quick-prompt-library D-4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* ONE FIELD'S NONSENSE MUST NOT TAKE ANOTHER ENTRY WITH IT. `config/ui-state.ts`
|
|
7
|
+
* wrote that rule for a two-field file; this is the same rule one level up, for a
|
|
8
|
+
* file the user may hand-edit and one bad character in which would otherwise cost
|
|
9
|
+
* them two hundred prompts. So every entry is normalised on its own, an entry that
|
|
10
|
+
* is structurally unusable is DROPPED AND COUNTED, and the top-level file is only
|
|
11
|
+
* discarded when its shape is unknown (a parse failure or a `schema` that is not
|
|
12
|
+
* `1`).
|
|
13
|
+
*
|
|
14
|
+
* LENGTH IS CLAMPED HERE, NOT ENFORCED. The write path refuses an over-long title
|
|
15
|
+
* (`PromptWriteError`) because the user can still fix it; the read path clamps,
|
|
16
|
+
* because by then the text is the only copy and dropping the entry to protect a
|
|
17
|
+
* ceiling would delete the user's work to keep a number tidy.
|
|
18
|
+
*/
|
|
19
|
+
import type { PromptLibrary, QuickPrompt } from './types.js';
|
|
20
|
+
/**
|
|
21
|
+
* One entry, or `null` when it cannot be salvaged.
|
|
22
|
+
*
|
|
23
|
+
* `id` IS REPAIRED, NOT TRUSTED. A duplicate id would make `{kind:'edit', id}`
|
|
24
|
+
* ambiguous and `deletePrompt(id)` delete two entries, so the caller passes the
|
|
25
|
+
* ids it has already claimed and an unrepairable one is replaced here.
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizePrompt(raw: unknown, now?: number): QuickPrompt | null;
|
|
28
|
+
export interface NormalisedLibrary {
|
|
29
|
+
library: PromptLibrary;
|
|
30
|
+
/** Entries that could not be salvaged, for the one-shot notice (§8). */
|
|
31
|
+
dropped: number;
|
|
32
|
+
/** Why the whole file was discarded. `'none'` means it was usable. */
|
|
33
|
+
repaired: 'none' | 'schema' | 'parse';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The whole file, with per-entry tolerance and one repair verdict.
|
|
37
|
+
*
|
|
38
|
+
* `repaired` is what the store uses to decide whether to write the `.bak` copy;
|
|
39
|
+
* keeping the verdict in the return value rather than in the store means the pure
|
|
40
|
+
* function stays pure and the decision stays testable.
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizePromptLibrary(raw: unknown, now?: number): NormalisedLibrary;
|
|
43
|
+
/**
|
|
44
|
+
* The canonical "most useful first" comparison (`/prompts sort favorite`).
|
|
45
|
+
*
|
|
46
|
+
* Favourite, then most used, then most recent, then title, then id. The last two
|
|
47
|
+
* are not decoration: without a total order two entries with identical counters
|
|
48
|
+
* would swap places between calls, and a "sorted" library that reorders itself on
|
|
49
|
+
* every read is worse than an unsorted one.
|
|
50
|
+
*/
|
|
51
|
+
export declare function comparePrompts(a: QuickPrompt, b: QuickPrompt): number;
|
|
52
|
+
/** Test seam: `normalize.ts` owns no state, but the name keeps the call sites honest. */
|
|
53
|
+
export declare function emptyPromptLibrary(): PromptLibrary;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the library off disk without trusting it (quick-prompt-library D-4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* ONE FIELD'S NONSENSE MUST NOT TAKE ANOTHER ENTRY WITH IT. `config/ui-state.ts`
|
|
7
|
+
* wrote that rule for a two-field file; this is the same rule one level up, for a
|
|
8
|
+
* file the user may hand-edit and one bad character in which would otherwise cost
|
|
9
|
+
* them two hundred prompts. So every entry is normalised on its own, an entry that
|
|
10
|
+
* is structurally unusable is DROPPED AND COUNTED, and the top-level file is only
|
|
11
|
+
* discarded when its shape is unknown (a parse failure or a `schema` that is not
|
|
12
|
+
* `1`).
|
|
13
|
+
*
|
|
14
|
+
* LENGTH IS CLAMPED HERE, NOT ENFORCED. The write path refuses an over-long title
|
|
15
|
+
* (`PromptWriteError`) because the user can still fix it; the read path clamps,
|
|
16
|
+
* because by then the text is the only copy and dropping the entry to protect a
|
|
17
|
+
* ceiling would delete the user's work to keep a number tidy.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { PROMPT_LIMITS } from './limits.js';
|
|
21
|
+
/** First `max` code points. */
|
|
22
|
+
function clampText(text, max) {
|
|
23
|
+
const points = [...text];
|
|
24
|
+
return points.length <= max ? text : points.slice(0, max).join('');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Control characters except `\n` and `\t`, and `\r` folded into `\n`.
|
|
28
|
+
*
|
|
29
|
+
* A `\r` that survives reaches the renderer as a carriage return, which moves the
|
|
30
|
+
* caret to column 0 and overwrites the row it is on; a stray `\u0000` is a frame
|
|
31
|
+
* marker in this codebase and must never appear inside stored content.
|
|
32
|
+
*/
|
|
33
|
+
function sanitiseText(text) {
|
|
34
|
+
return text
|
|
35
|
+
.replace(/\r\n?/g, '\n')
|
|
36
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '');
|
|
37
|
+
}
|
|
38
|
+
function asString(value) {
|
|
39
|
+
return typeof value === 'string' ? value : null;
|
|
40
|
+
}
|
|
41
|
+
function asCount(value, fallback) {
|
|
42
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
43
|
+
return fallback;
|
|
44
|
+
return Math.floor(value);
|
|
45
|
+
}
|
|
46
|
+
function asTimestamp(value, fallback) {
|
|
47
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
|
|
48
|
+
return fallback;
|
|
49
|
+
return Math.floor(value);
|
|
50
|
+
}
|
|
51
|
+
/** Whitespace-folded, deduplicated, clamped, bounded. Order is preserved. */
|
|
52
|
+
function normaliseTags(value) {
|
|
53
|
+
if (!Array.isArray(value))
|
|
54
|
+
return [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
const tags = [];
|
|
57
|
+
for (const raw of value) {
|
|
58
|
+
const text = asString(raw);
|
|
59
|
+
if (text === null)
|
|
60
|
+
continue;
|
|
61
|
+
const tag = clampText(text.trim().replace(/\s+/g, ' '), PROMPT_LIMITS.tagMaxChars);
|
|
62
|
+
if (tag.length === 0 || seen.has(tag))
|
|
63
|
+
continue;
|
|
64
|
+
seen.add(tag);
|
|
65
|
+
tags.push(tag);
|
|
66
|
+
if (tags.length >= PROMPT_LIMITS.maxTags)
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
return tags;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One entry, or `null` when it cannot be salvaged.
|
|
73
|
+
*
|
|
74
|
+
* `id` IS REPAIRED, NOT TRUSTED. A duplicate id would make `{kind:'edit', id}`
|
|
75
|
+
* ambiguous and `deletePrompt(id)` delete two entries, so the caller passes the
|
|
76
|
+
* ids it has already claimed and an unrepairable one is replaced here.
|
|
77
|
+
*/
|
|
78
|
+
export function normalizePrompt(raw, now = Date.now()) {
|
|
79
|
+
if (!raw || typeof raw !== 'object')
|
|
80
|
+
return null;
|
|
81
|
+
const src = raw;
|
|
82
|
+
const titleRaw = asString(src.title);
|
|
83
|
+
const contentRaw = asString(src.content);
|
|
84
|
+
if (titleRaw === null || contentRaw === null)
|
|
85
|
+
return null;
|
|
86
|
+
const title = clampText(sanitiseText(titleRaw).trim(), PROMPT_LIMITS.titleMaxChars);
|
|
87
|
+
const content = clampText(sanitiseText(contentRaw), PROMPT_LIMITS.contentMaxChars);
|
|
88
|
+
// I-5: neither field is ever the empty string, so an entry with no body left
|
|
89
|
+
// after sanitising is not an entry.
|
|
90
|
+
if (title.length === 0 || content.trim().length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
const id = asString(src.id);
|
|
93
|
+
const description = clampText(sanitiseText(asString(src.description) ?? '').trim(), PROMPT_LIMITS.descriptionMaxChars);
|
|
94
|
+
const createdAt = asTimestamp(src.createdAt, now);
|
|
95
|
+
const updatedAt = asTimestamp(src.updatedAt, createdAt);
|
|
96
|
+
const entry = {
|
|
97
|
+
id: id !== null && id.trim().length > 0 ? id : randomUUID(),
|
|
98
|
+
title,
|
|
99
|
+
content,
|
|
100
|
+
useCount: asCount(src.useCount, 0),
|
|
101
|
+
createdAt,
|
|
102
|
+
updatedAt: Math.max(createdAt, updatedAt),
|
|
103
|
+
};
|
|
104
|
+
if (description.length > 0)
|
|
105
|
+
entry.description = description;
|
|
106
|
+
const tags = normaliseTags(src.tags);
|
|
107
|
+
if (tags.length > 0)
|
|
108
|
+
entry.tags = tags;
|
|
109
|
+
if (src.favorite === true)
|
|
110
|
+
entry.favorite = true;
|
|
111
|
+
const lastUsedAt = asTimestamp(src.lastUsedAt, 0);
|
|
112
|
+
if (lastUsedAt > 0)
|
|
113
|
+
entry.lastUsedAt = lastUsedAt;
|
|
114
|
+
return entry;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The whole file, with per-entry tolerance and one repair verdict.
|
|
118
|
+
*
|
|
119
|
+
* `repaired` is what the store uses to decide whether to write the `.bak` copy;
|
|
120
|
+
* keeping the verdict in the return value rather than in the store means the pure
|
|
121
|
+
* function stays pure and the decision stays testable.
|
|
122
|
+
*/
|
|
123
|
+
export function normalizePromptLibrary(raw, now = Date.now()) {
|
|
124
|
+
const empty = (repaired) => ({
|
|
125
|
+
library: { schema: 1, prompts: [], updatedAt: 0 },
|
|
126
|
+
dropped: 0,
|
|
127
|
+
repaired,
|
|
128
|
+
});
|
|
129
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
130
|
+
return empty('parse');
|
|
131
|
+
const src = raw;
|
|
132
|
+
// A `schema` mismatch DISCARDS NOTHING on disk: the store copies the file to
|
|
133
|
+
// `<name>.bak` first (D-4). Reporting an empty library here is about the shape
|
|
134
|
+
// this process can read, not about what the user is allowed to get back.
|
|
135
|
+
if (src.schema !== 1)
|
|
136
|
+
return empty('schema');
|
|
137
|
+
if (!Array.isArray(src.prompts))
|
|
138
|
+
return empty('parse');
|
|
139
|
+
const entries = src.prompts;
|
|
140
|
+
const prompts = [];
|
|
141
|
+
const claimed = new Set();
|
|
142
|
+
let dropped = 0;
|
|
143
|
+
for (let at = 0; at < entries.length; at += 1) {
|
|
144
|
+
if (prompts.length >= PROMPT_LIMITS.maxPrompts) {
|
|
145
|
+
// THE CEILING APPLIES ON READ TOO, and the leftovers are COUNTED rather than
|
|
146
|
+
// silently forgotten: a hand-edited 300-entry file would lose 100 entries on
|
|
147
|
+
// the next write, so the user has to be told that this happened.
|
|
148
|
+
dropped += entries.length - at;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
const entry = normalizePrompt(entries[at], now);
|
|
152
|
+
if (!entry) {
|
|
153
|
+
dropped += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (claimed.has(entry.id))
|
|
157
|
+
entry.id = randomUUID();
|
|
158
|
+
claimed.add(entry.id);
|
|
159
|
+
prompts.push(entry);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
library: { schema: 1, prompts, updatedAt: asTimestamp(src.updatedAt, 0) },
|
|
163
|
+
dropped,
|
|
164
|
+
repaired: 'none',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The canonical "most useful first" comparison (`/prompts sort favorite`).
|
|
169
|
+
*
|
|
170
|
+
* Favourite, then most used, then most recent, then title, then id. The last two
|
|
171
|
+
* are not decoration: without a total order two entries with identical counters
|
|
172
|
+
* would swap places between calls, and a "sorted" library that reorders itself on
|
|
173
|
+
* every read is worse than an unsorted one.
|
|
174
|
+
*/
|
|
175
|
+
export function comparePrompts(a, b) {
|
|
176
|
+
if ((a.favorite === true) !== (b.favorite === true))
|
|
177
|
+
return a.favorite === true ? -1 : 1;
|
|
178
|
+
if (a.useCount !== b.useCount)
|
|
179
|
+
return b.useCount - a.useCount;
|
|
180
|
+
const recent = (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0);
|
|
181
|
+
if (recent !== 0)
|
|
182
|
+
return recent;
|
|
183
|
+
if (a.title !== b.title)
|
|
184
|
+
return a.title < b.title ? -1 : 1;
|
|
185
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
186
|
+
}
|
|
187
|
+
/** Test seam: `normalize.ts` owns no state, but the name keeps the call sites honest. */
|
|
188
|
+
export function emptyPromptLibrary() {
|
|
189
|
+
return { schema: 1, prompts: [], updatedAt: 0 };
|
|
190
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<n|title>` -> one entry (quick-prompt-library D-22).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* TITLES MAY REPEAT (I-2), so this function has exactly one job the command layer
|
|
7
|
+
* cannot do for itself: turn a possibly-ambiguous reference into either one entry
|
|
8
|
+
* or a NAMED failure with candidates. It never picks the first of several.
|
|
9
|
+
*
|
|
10
|
+
* WHY THAT IS NOT PEDANTRY: `/prompts rm <title>` has no recycle bin (D-13), so
|
|
11
|
+
* "the first of the two entries with that title" is a one-keystroke way to delete
|
|
12
|
+
* the wrong prompt - and the user would have no way to tell which one they had
|
|
13
|
+
* been talking to. A refusal that PRINTS the candidates costs one more command and
|
|
14
|
+
* keeps the data.
|
|
15
|
+
*/
|
|
16
|
+
import type { PromptLibrary, PromptRefResolution } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a reference, in this fixed order:
|
|
19
|
+
*
|
|
20
|
+
* 1. a pure decimal number is a 1-based INDEX (never a title), because a script
|
|
21
|
+
* that says `2` must never depend on what the second prompt is called;
|
|
22
|
+
* 2. an exact title (case-sensitive, surrounding whitespace removed);
|
|
23
|
+
* 3. a unique title PREFIX, so `clean` finds `clean code review`.
|
|
24
|
+
*
|
|
25
|
+
* A number that is out of range is `not_found` rather than falling through to the
|
|
26
|
+
* title rules: `0` and `999` are index mistakes, and treating them as titles would
|
|
27
|
+
* silently match a prompt literally named "999".
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolvePromptRef(library: PromptLibrary, ref: string): PromptRefResolution;
|
|
30
|
+
/** The index a resolved entry sits at, for the "3 days ago" style detail lines. */
|
|
31
|
+
export declare function promptIndexOf(library: PromptLibrary, id: string): number;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<n|title>` -> one entry (quick-prompt-library D-22).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* TITLES MAY REPEAT (I-2), so this function has exactly one job the command layer
|
|
7
|
+
* cannot do for itself: turn a possibly-ambiguous reference into either one entry
|
|
8
|
+
* or a NAMED failure with candidates. It never picks the first of several.
|
|
9
|
+
*
|
|
10
|
+
* WHY THAT IS NOT PEDANTRY: `/prompts rm <title>` has no recycle bin (D-13), so
|
|
11
|
+
* "the first of the two entries with that title" is a one-keystroke way to delete
|
|
12
|
+
* the wrong prompt - and the user would have no way to tell which one they had
|
|
13
|
+
* been talking to. A refusal that PRINTS the candidates costs one more command and
|
|
14
|
+
* keeps the data.
|
|
15
|
+
*/
|
|
16
|
+
/** How many candidates a refusal names. Five is what a terminal row shows. */
|
|
17
|
+
const MAX_CANDIDATES = 5;
|
|
18
|
+
function exactTitle(prompts, ref) {
|
|
19
|
+
return prompts.filter((prompt) => prompt.title === ref);
|
|
20
|
+
}
|
|
21
|
+
function prefixTitle(prompts, ref) {
|
|
22
|
+
return prompts.filter((prompt) => prompt.title.startsWith(ref));
|
|
23
|
+
}
|
|
24
|
+
function decide(matches) {
|
|
25
|
+
if (matches.length === 1)
|
|
26
|
+
return { ok: true, prompt: matches[0] };
|
|
27
|
+
if (matches.length === 0)
|
|
28
|
+
return { ok: false, code: 'not_found' };
|
|
29
|
+
return { ok: false, code: 'ambiguous', candidates: matches.slice(0, MAX_CANDIDATES) };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a reference, in this fixed order:
|
|
33
|
+
*
|
|
34
|
+
* 1. a pure decimal number is a 1-based INDEX (never a title), because a script
|
|
35
|
+
* that says `2` must never depend on what the second prompt is called;
|
|
36
|
+
* 2. an exact title (case-sensitive, surrounding whitespace removed);
|
|
37
|
+
* 3. a unique title PREFIX, so `clean` finds `clean code review`.
|
|
38
|
+
*
|
|
39
|
+
* A number that is out of range is `not_found` rather than falling through to the
|
|
40
|
+
* title rules: `0` and `999` are index mistakes, and treating them as titles would
|
|
41
|
+
* silently match a prompt literally named "999".
|
|
42
|
+
*/
|
|
43
|
+
export function resolvePromptRef(library, ref) {
|
|
44
|
+
const trimmed = ref.trim();
|
|
45
|
+
if (trimmed.length === 0)
|
|
46
|
+
return { ok: false, code: 'not_found' };
|
|
47
|
+
if (/^\d+$/.test(trimmed)) {
|
|
48
|
+
const at = Number.parseInt(trimmed, 10) - 1;
|
|
49
|
+
const prompt = library.prompts[at];
|
|
50
|
+
return prompt ? { ok: true, prompt } : { ok: false, code: 'not_found' };
|
|
51
|
+
}
|
|
52
|
+
const exact = exactTitle(library.prompts, trimmed);
|
|
53
|
+
if (exact.length > 0)
|
|
54
|
+
return decide(exact);
|
|
55
|
+
return decide(prefixTitle(library.prompts, trimmed));
|
|
56
|
+
}
|
|
57
|
+
/** The index a resolved entry sits at, for the "3 days ago" style detail lines. */
|
|
58
|
+
export function promptIndexOf(library, id) {
|
|
59
|
+
return library.prompts.findIndex((prompt) => prompt.id === id);
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filtering and view ordering for the picker (quick-prompt-library D-9 / D-10).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* SUBSEQUENCE MATCHING, NOT SUBSTRING MATCHING. `clr` should find `clean code
|
|
7
|
+
* review`, and Chinese titles make the same demand in a different shape: the
|
|
8
|
+
* characters a user remembers are rarely contiguous. The query is matched
|
|
9
|
+
* case-insensitively against the title, then the tags, then the description, and
|
|
10
|
+
* the FIELD a match landed in is a stronger signal than how good the match was.
|
|
11
|
+
*
|
|
12
|
+
* NO CHARACTER HIGHLIGHTING (D-10). Colouring the matched characters would mean
|
|
13
|
+
* splitting a row into styled runs, and every row in this package is one
|
|
14
|
+
* `wrap="truncate"` `<Text>` precisely so its height is exactly one row. The
|
|
15
|
+
* detail block answers "why did this match" instead.
|
|
16
|
+
*/
|
|
17
|
+
import type { QuickPrompt, PromptSortView } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Greedy subsequence match; `null` when `query` is not a subsequence of `text`.
|
|
20
|
+
*
|
|
21
|
+
* The score is LOWER-is-better and is built from what a reader would actually
|
|
22
|
+
* call a good match: how early the first hit is, how tightly the hits cluster,
|
|
23
|
+
* and how many characters had to be skipped between them.
|
|
24
|
+
*/
|
|
25
|
+
export declare function subsequenceScore(text: string, query: string): number | null;
|
|
26
|
+
/**
|
|
27
|
+
* Filter, but NEVER REORDER BEYOND WHAT THE QUERY EARNS.
|
|
28
|
+
*
|
|
29
|
+
* Ties keep the library's own order, because a filter that reshuffles entries with
|
|
30
|
+
* equal scores makes the list move under the user's arrow keys while they type.
|
|
31
|
+
* Favourites win ties, which is the only preference the picker states.
|
|
32
|
+
*/
|
|
33
|
+
export declare function searchPrompts(prompts: readonly QuickPrompt[], query: string): {
|
|
34
|
+
prompt: QuickPrompt;
|
|
35
|
+
score: number;
|
|
36
|
+
}[];
|
|
37
|
+
/**
|
|
38
|
+
* The three presentations (D-9).
|
|
39
|
+
*
|
|
40
|
+
* `manual` is the only persisted order; the other two are VANISHING views. The one
|
|
41
|
+
* that rearranges is `favorite` - `recent` sorts by `lastUsedAt` only, so an entry
|
|
42
|
+
* that has never been used keeps its place at the tail instead of jumping around
|
|
43
|
+
* while the user watches.
|
|
44
|
+
*/
|
|
45
|
+
export declare function viewOrder(prompts: readonly QuickPrompt[], view: PromptSortView): QuickPrompt[];
|
|
46
|
+
/** The next view in the `Ctrl+S` cycle (manual -> recent -> favorite -> manual). */
|
|
47
|
+
export declare function nextView(view: PromptSortView): PromptSortView;
|
|
48
|
+
/**
|
|
49
|
+
* Title order, for the `/prompts sort name` write.
|
|
50
|
+
*
|
|
51
|
+
* A TOTAL order (title, then id): a sort the user asked for has to be reproducible, and
|
|
52
|
+
* two entries sharing a title would otherwise swap places between runs - a "sorted"
|
|
53
|
+
* library that reorders itself on every read is worse than an unsorted one. Code-unit
|
|
54
|
+
* comparison rather than `localeCompare`, because the file is written on one machine and
|
|
55
|
+
* read on the next, and a locale-dependent order would silently rewrite the user's
|
|
56
|
+
* sequence on a different machine for no reason they could see.
|
|
57
|
+
*/
|
|
58
|
+
export declare function sortedPromptTitleOrder(prompts: readonly QuickPrompt[]): QuickPrompt[];
|