@plannotator/ui 0.45.1 → 0.46.0
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/HANDOFF.md +51 -1
- package/README.md +1 -1
- package/components/AgentsTab.tsx +108 -108
- package/components/BlockRenderer.tsx +9 -5
- package/components/ExportModal.tsx +8 -4
- package/components/ImageAnnotator/Toolbar.tsx +13 -3
- package/components/ImageAnnotator/index.tsx +6 -2
- package/components/InlineMarkdown.tsx +14 -7
- package/components/RenderedMarkdown.tsx +3 -1
- package/components/Settings.tsx +38 -15
- package/components/Viewer.tsx +76 -18
- package/components/blocks/AlertBlock.tsx +3 -2
- package/components/blocks/Callout.tsx +3 -0
- package/components/blocks/CodeBlock.tsx +6 -2
- package/components/blocks/TableBlock.tsx +4 -0
- package/components/blocks/TablePopout.tsx +6 -2
- package/components/blocks/proseBody.tsx +3 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +6 -2
- package/config/reviewView.ts +8 -3
- package/config/settings.ts +10 -4
- package/hooks/useAgentSettings.ts +108 -123
- package/hooks/useAnnotationHighlighter.ts +109 -1
- package/hooks/useCodeAnnotationDraft.ts +196 -3
- package/hooks/useModelCatalogs.ts +116 -0
- package/package.json +2 -2
- package/styles.css +1 -1
- package/types.ts +31 -0
- package/utils/aiProvider.ts +8 -16
- package/utils/annotateAgentInstructions.ts +162 -0
- package/utils/codeHighlight.ts +18 -0
- package/utils/parser.ts +141 -31
- package/utils/terminalToolsAnnouncement.ts +2 -2
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/utils/codexModels.ts +0 -79
package/types.ts
CHANGED
|
@@ -348,6 +348,37 @@ export interface CodeAnnotation {
|
|
|
348
348
|
gitButlerBase?: string;
|
|
349
349
|
/** Exact server snapshot that supplied the GitButler line coordinates. */
|
|
350
350
|
gitButlerSnapshotId?: string;
|
|
351
|
+
/**
|
|
352
|
+
* PR reviews only (#1590): the text of the diff lines this line comment
|
|
353
|
+
* was anchored to at creation (lineStart..lineEnd on `side`, joined with
|
|
354
|
+
* "\n"). When a saved draft is restored against a different patch, the
|
|
355
|
+
* comment keeps its position only if these lines still read the same;
|
|
356
|
+
* otherwise it is marked `outdated`. Absent on older annotations and on
|
|
357
|
+
* lines outside the patch hunks.
|
|
358
|
+
*/
|
|
359
|
+
anchorText?: string;
|
|
360
|
+
/**
|
|
361
|
+
* PR reviews only (#1590): the lines around the anchor on the same side
|
|
362
|
+
* (up to two before and two after; `null` where a line is outside the patch
|
|
363
|
+
* hunks) and the hunk header's function context (`hunk`, when git printed
|
|
364
|
+
* one), so a comment on a common line (`}`, `return null;`) only keeps its
|
|
365
|
+
* position when its surroundings still match too.
|
|
366
|
+
*/
|
|
367
|
+
anchorContext?: { before: (string | null)[]; after: (string | null)[]; hunk?: string };
|
|
368
|
+
/**
|
|
369
|
+
* PR reviews only (#1590): the review snapshot id of the diff whose line
|
|
370
|
+
* coordinates this comment uses. Re-stamped when a later diff passes the
|
|
371
|
+
* anchor check; a line comment is only posted inline when this matches the
|
|
372
|
+
* diff currently known for its PR.
|
|
373
|
+
*/
|
|
374
|
+
anchorSnapshot?: string;
|
|
375
|
+
/**
|
|
376
|
+
* Set when a restored PR draft's comment no longer matches the code it was
|
|
377
|
+
* written on (the PR changed between sessions). Its line numbers refer to
|
|
378
|
+
* the earlier version: it is listed and exported (labelled) but never drawn
|
|
379
|
+
* inline on the current diff and never posted as an inline PR comment.
|
|
380
|
+
*/
|
|
381
|
+
outdated?: boolean;
|
|
351
382
|
}
|
|
352
383
|
|
|
353
384
|
/** Token-level metadata passed from selection to annotation creation. */
|
package/utils/aiProvider.ts
CHANGED
|
@@ -8,20 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
import { storage } from './storage';
|
|
10
10
|
import { AGENT_CONFIG, getAgentAIProviderTypes, type Origin } from '@plannotator/core/agents';
|
|
11
|
+
import { resolveModelChoice, type CatalogModel } from '@plannotator/core/model-catalog';
|
|
11
12
|
|
|
12
13
|
const PROVIDER_KEY = 'plannotator-ai-provider';
|
|
13
14
|
const MODELS_KEY = 'plannotator-ai-models';
|
|
14
15
|
const PROVIDER_BY_ORIGIN_KEY = 'plannotator-ai-provider-by-origin';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
label: string;
|
|
19
|
-
default?: boolean;
|
|
20
|
-
/** Reasoning-effort options this model supports (provider-reported, e.g. Codex). */
|
|
21
|
-
reasoningEfforts?: { id: string; label: string }[];
|
|
22
|
-
/** The model's default reasoning effort. */
|
|
23
|
-
defaultReasoningEffort?: string;
|
|
24
|
-
}
|
|
17
|
+
/** A provider-reported model — the shared catalog shape. */
|
|
18
|
+
export type AIProviderModel = CatalogModel;
|
|
25
19
|
|
|
26
20
|
export interface AIProviderOption {
|
|
27
21
|
id: string;
|
|
@@ -135,13 +129,11 @@ export function resolveAIModelForProvider(
|
|
|
135
129
|
): string | null {
|
|
136
130
|
if (!provider) return null;
|
|
137
131
|
const models = provider.models ?? [];
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const defaultModel = models.find(m => m.default) ?? models[0];
|
|
144
|
-
return defaultModel?.id ?? null;
|
|
132
|
+
const preferredModel = preferredModels[provider.id] ?? '';
|
|
133
|
+
// Providers that report no models take the saved pick verbatim; otherwise
|
|
134
|
+
// the shared resolver (same one the server and the launchers use).
|
|
135
|
+
if (models.length === 0) return preferredModel || null;
|
|
136
|
+
return resolveModelChoice(preferredModel, models) || null;
|
|
145
137
|
}
|
|
146
138
|
|
|
147
139
|
export function resolveAIProviderSelection(options: {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the clipboard payload that teaches an external agent how to post
|
|
3
|
+
* annotations into a live Plannotator **annotate** session via
|
|
4
|
+
* /api/external-annotations. The annotate twin of `planAgentInstructions.ts`:
|
|
5
|
+
* same endpoint and wire shape (annotate servers run the plan-mode
|
|
6
|
+
* validator, `transformPlanInput` in `@plannotator/core/external-annotation`),
|
|
7
|
+
* but it talks about a document instead of a plan, carries no deny/resubmit
|
|
8
|
+
* loop, and adds one short section for the surface the session is showing.
|
|
9
|
+
*
|
|
10
|
+
* Only document what the validator accepts: POST takes `source`, `type`,
|
|
11
|
+
* `text`, `originalText`, `author`, and a `diagramAnchor`; an element anchor
|
|
12
|
+
* (`htmlAnchor`) is accepted by PATCH only.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type AnnotateInstructionsSurface =
|
|
16
|
+
/** Markdown / plain-text file, URL, or converted HTML. */
|
|
17
|
+
| 'markdown'
|
|
18
|
+
/** Raw HTML file rendered as the page. */
|
|
19
|
+
| 'html'
|
|
20
|
+
/** A running local app mirrored through the live proxy. */
|
|
21
|
+
| 'live-app'
|
|
22
|
+
/** A whole-file Mermaid / Graphviz source rendered as one diagram. */
|
|
23
|
+
| 'diagram'
|
|
24
|
+
/** A folder session (file browser, one document open at a time). */
|
|
25
|
+
| 'folder'
|
|
26
|
+
/** annotate-last: the agent's own message. */
|
|
27
|
+
| 'message';
|
|
28
|
+
|
|
29
|
+
interface SurfaceSection {
|
|
30
|
+
/** The one command that fetches what the user is looking at. */
|
|
31
|
+
read: (origin: string) => string;
|
|
32
|
+
/** Surface-specific targeting rules, printed after the read command. */
|
|
33
|
+
notes: (origin: string) => string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SURFACES: Record<AnnotateInstructionsSurface, SurfaceSection> = {
|
|
37
|
+
markdown: {
|
|
38
|
+
read: (origin) => `curl -s ${origin}/api/plan | jq -r .plan`,
|
|
39
|
+
notes: () => `\`originalText\` is matched against the rendered text, so leave out markdown syntax such as \`**\` or \`#\`.`,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
message: {
|
|
43
|
+
read: (origin) => `curl -s ${origin}/api/plan | jq -r .plan`,
|
|
44
|
+
notes: () => `The document is an agent message. \`originalText\` is matched against the rendered message text.`,
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
html: {
|
|
48
|
+
read: (origin) => `curl -s ${origin}/api/plan | jq -r .rawHtml`,
|
|
49
|
+
notes: (origin) => `The document is an HTML page rendered as a real web page. Quote its **visible text**, never markup: \`originalText\` is found by a text search of the rendered page.
|
|
50
|
+
|
|
51
|
+
To pin a comment to an element instead of a phrase, POST it first, then attach an element anchor with PATCH (POST ignores \`htmlAnchor\`):
|
|
52
|
+
|
|
53
|
+
\`\`\`sh
|
|
54
|
+
curl -s -X PATCH "${origin}/api/external-annotations?id=<uuid>" \\
|
|
55
|
+
-H 'Content-Type: application/json' \\
|
|
56
|
+
-d '{"htmlAnchor": {"selector": "#pricing > h2", "tagName": "h2", "text": "Pricing"}}'
|
|
57
|
+
\`\`\`
|
|
58
|
+
|
|
59
|
+
\`selector\` must match exactly one element; \`text\`, when given, is that element's visible text and is checked when the marker is restored.`,
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
'live-app': {
|
|
63
|
+
read: (origin) => `curl -s ${origin}/api/plan | jq -r .targetUrl # the running app; open or fetch it`,
|
|
64
|
+
notes: () => `The user is annotating a running local app, so there is no document text in the API. Quote **visible text** from the app: it is highlighted wherever it is found on the page the user has open. A comment you post shows on every page; to tie it to one route, PATCH \`{"pageUrl": "/settings?tab=2"}\` (path plus query). Prefer \`GLOBAL_COMMENT\` for anything not about specific on-screen text.`,
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
diagram: {
|
|
68
|
+
read: (origin) => `curl -s ${origin}/api/plan | jq -r .plan`,
|
|
69
|
+
notes: () => `The document is one Mermaid or Graphviz diagram (the source above). To comment on a node, edge, or cluster, add a \`diagramAnchor\` naming it by its id in the source; \`originalText\` is the part's label:
|
|
70
|
+
|
|
71
|
+
\`\`\`json
|
|
72
|
+
{"source": "claude-code", "type": "COMMENT", "text": "Rename this step.",
|
|
73
|
+
"originalText": "Start", "diagramAnchor": {"v": 1, "family": "flowchart", "kind": "node", "id": "A", "label": "Start"}}
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
\`family\` is one of flowchart, state, class, er, requirement, sequence, other, graphviz; \`kind\` is node, edge (use \`from\` + \`to\` instead of \`id\`), cluster, or diagram (the whole diagram, no id). A malformed anchor is a 400; one that names no part of the diagram lists as unanchored.`,
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
folder: {
|
|
80
|
+
read: (origin) => `curl -s "${origin}/api/doc?path=<path-relative-to-folder>&doc=1" | jq -r '.markdown // .rawHtml'`,
|
|
81
|
+
notes: (origin) => `The user browses a folder (\`curl -s ${origin}/api/plan | jq -r .filePath\`) and opens one document at a time. No API tells you which document is open.
|
|
82
|
+
|
|
83
|
+
External comments cannot target a specific document. Every comment you post is a session-level entry: it lists in the annotations panel whichever document is open (or none), is **not** highlighted inline in folder documents, and is included in the feedback the user sends. Name the file in \`text\` and prefer \`GLOBAL_COMMENT\`.`,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export function buildAnnotateAgentInstructions(
|
|
88
|
+
origin: string,
|
|
89
|
+
surface: AnnotateInstructionsSurface = 'markdown',
|
|
90
|
+
): string {
|
|
91
|
+
const section = SURFACES[surface];
|
|
92
|
+
return `# Plannotator — External Annotations (annotate session)
|
|
93
|
+
|
|
94
|
+
You can leave review comments on the document the user is annotating by POSTing to a small HTTP API. They appear immediately in the user's annotations panel (and as highlights where the quoted text is found). The user decides what to send back to their agent; there is no approve, deny, or submit endpoint for you.
|
|
95
|
+
|
|
96
|
+
## Base URL
|
|
97
|
+
${origin}
|
|
98
|
+
|
|
99
|
+
All endpoints below are relative to that base. No authentication.
|
|
100
|
+
|
|
101
|
+
## Read the document
|
|
102
|
+
|
|
103
|
+
\`\`\`sh
|
|
104
|
+
${section.read(origin)}
|
|
105
|
+
\`\`\`
|
|
106
|
+
|
|
107
|
+
Line numbers do not apply. Inline comments are pinned by quoting a verbatim phrase in \`originalText\`. ${section.notes(origin)}
|
|
108
|
+
|
|
109
|
+
## Post comments
|
|
110
|
+
|
|
111
|
+
\`\`\`sh
|
|
112
|
+
curl -s ${origin}/api/external-annotations \\
|
|
113
|
+
-H 'Content-Type: application/json' \\
|
|
114
|
+
-d '{
|
|
115
|
+
"annotations": [
|
|
116
|
+
{"source": "claude-code", "type": "COMMENT", "text": "This claim needs a source.", "originalText": "adoption doubled last year"},
|
|
117
|
+
{"source": "claude-code", "type": "GLOBAL_COMMENT", "text": "The intro and summary contradict each other."}
|
|
118
|
+
]
|
|
119
|
+
}'
|
|
120
|
+
\`\`\`
|
|
121
|
+
|
|
122
|
+
A single annotation can be posted without the \`annotations\` wrapper. Returns \`201 {"ids": [...]}\`, or \`400 {"error": "..."}\` (batches are all-or-nothing).
|
|
123
|
+
|
|
124
|
+
| Field | Required | Notes |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| \`source\` | yes | Stable identifier for you (e.g. \`"claude-code"\`); reuse it so you can clean up later. |
|
|
127
|
+
| \`text\` | yes | The comment body. |
|
|
128
|
+
| \`type\` | yes | \`"COMMENT"\` (pinned to \`originalText\`) or \`"GLOBAL_COMMENT"\` (panel only). |
|
|
129
|
+
| \`originalText\` | for \`COMMENT\` | A verbatim phrase from the document. If it is not found, the comment stays in the panel without a highlight. |
|
|
130
|
+
| \`author\` | no | Label shown next to the comment. |
|
|
131
|
+
|
|
132
|
+
## List, edit, delete
|
|
133
|
+
|
|
134
|
+
\`\`\`sh
|
|
135
|
+
curl -s ${origin}/api/external-annotations | jq # list
|
|
136
|
+
curl -s -X PATCH "${origin}/api/external-annotations?id=<uuid>" \\
|
|
137
|
+
-H 'Content-Type: application/json' -d '{"text": "Reworded."}' # edit
|
|
138
|
+
curl -s -X DELETE "${origin}/api/external-annotations?source=claude-code" # remove yours before re-posting
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
## Notes
|
|
142
|
+
- Posting the same comment twice creates two entries; delete by \`source\` before a re-run.
|
|
143
|
+
- The document can change while the session is open; re-read it before re-posting.
|
|
144
|
+
- This API is local to the user's machine. Treat it as a UI surface, not a public service.
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Which surface section an annotate session's instructions carry. A folder
|
|
149
|
+
* session is a folder whatever file type is open (the scoping rule is what
|
|
150
|
+
* the agent needs to know); a live app wins over its HTML render. */
|
|
151
|
+
export function resolveAnnotateInstructionsSurface(input: {
|
|
152
|
+
liveApp: boolean;
|
|
153
|
+
annotateSource: 'file' | 'message' | 'folder' | null;
|
|
154
|
+
renderAs: string;
|
|
155
|
+
}): AnnotateInstructionsSurface {
|
|
156
|
+
if (input.liveApp) return 'live-app';
|
|
157
|
+
if (input.annotateSource === 'folder') return 'folder';
|
|
158
|
+
if (input.annotateSource === 'message') return 'message';
|
|
159
|
+
if (input.renderAs === 'html') return 'html';
|
|
160
|
+
if (input.renderAs === 'mermaid' || input.renderAs === 'graphviz') return 'diagram';
|
|
161
|
+
return 'markdown';
|
|
162
|
+
}
|
package/utils/codeHighlight.ts
CHANGED
|
@@ -41,6 +41,24 @@ export function codeBlockClassName(language?: string): string {
|
|
|
41
41
|
return `${CODE_BLOCK_CLASS} font-mono${language ? ` language-${language}` : ''}`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Plan fences soft-wrap long lines (`white-space: pre-wrap`), but a fence laid
|
|
46
|
+
* out as a grid — box-drawing diagrams, `+---+` boxes, space-aligned columns —
|
|
47
|
+
* is unreadable once a row wraps. Those keep the old unwrapped, horizontally
|
|
48
|
+
* scrolling layout: the renderers mark them with `data-keep-layout`, which the
|
|
49
|
+
* editor stylesheet turns back into `white-space: pre`.
|
|
50
|
+
*
|
|
51
|
+
* Signals: any box-drawing or block-element character, a tab between two
|
|
52
|
+
* words, or a run of two or more spaces between two words (column alignment).
|
|
53
|
+
* A double space after sentence punctuation is prose, not alignment, and
|
|
54
|
+
* leading indentation is never interior, so ordinary code still wraps.
|
|
55
|
+
*/
|
|
56
|
+
const ALIGNED_LAYOUT = /[\u2500-\u259F]|\S\t+\S|[^\s.!?] {2,}\S/;
|
|
57
|
+
|
|
58
|
+
export function codeBlockKeepsLayout(content: string): boolean {
|
|
59
|
+
return ALIGNED_LAYOUT.test(content);
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
/** Shiki's `FontStyle` bitmask. Inlined so this module needs no shiki types. */
|
|
45
63
|
const FONT_STYLE_ITALIC = 1;
|
|
46
64
|
const FONT_STYLE_BOLD = 2;
|
package/utils/parser.ts
CHANGED
|
@@ -4,11 +4,19 @@ import { resolveReplyParents } from '@plannotator/core/annotation-threads';
|
|
|
4
4
|
import { diagramAnchorLocationLine, parseDiagramAnchor } from '@plannotator/core/diagram-anchor';
|
|
5
5
|
import { skillReferenceExportBlock } from './skillReferences';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Parsed YAML frontmatter value: scalar string, array, or nested map.
|
|
9
|
+
*/
|
|
10
|
+
export type FrontmatterValue =
|
|
11
|
+
| string
|
|
12
|
+
| FrontmatterValue[]
|
|
13
|
+
| { [key: string]: FrontmatterValue };
|
|
14
|
+
|
|
7
15
|
/**
|
|
8
16
|
* Parsed YAML frontmatter as key-value pairs.
|
|
9
17
|
*/
|
|
10
18
|
export interface Frontmatter {
|
|
11
|
-
[key: string]:
|
|
19
|
+
[key: string]: FrontmatterValue;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
/** Number of leading whitespace characters on a line. */
|
|
@@ -81,6 +89,38 @@ function parseBlockScalar(
|
|
|
81
89
|
return { value, endIndex: j - 1 };
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Parse a simple `key: value` pair from a line. Returns null if the line
|
|
94
|
+
* is not a valid YAML mapping entry (e.g. scalar URLs or quoted strings).
|
|
95
|
+
*/
|
|
96
|
+
function parseKeyValue(str: string): { key: string; value: string } | null {
|
|
97
|
+
// A QUOTED KEY is still a mapping entry: `"title": "Doc"` must yield
|
|
98
|
+
// { title: "Doc" }, not nothing. Only a line that is nothing but a quoted
|
|
99
|
+
// scalar (`"just a string"`) is not an entry — which is the case the char
|
|
100
|
+
// after the closing quote distinguishes.
|
|
101
|
+
const quote = str[0];
|
|
102
|
+
if (quote === '"' || quote === "'") {
|
|
103
|
+
const closing = str.indexOf(quote, 1);
|
|
104
|
+
if (closing === -1) return null;
|
|
105
|
+
if (str[closing + 1] !== ':') return null;
|
|
106
|
+
// Same rule the unquoted branch applies: `key:value` is a scalar, not a
|
|
107
|
+
// mapping entry. A colon at end of line (empty value) is fine.
|
|
108
|
+
const afterColon = str[closing + 2];
|
|
109
|
+
if (afterColon !== undefined && afterColon !== ' ' && afterColon !== '\t') {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return { key: str.slice(1, closing), value: str.slice(closing + 2).trim() };
|
|
113
|
+
}
|
|
114
|
+
const colonIndex = str.indexOf(':');
|
|
115
|
+
if (colonIndex <= 0) return null;
|
|
116
|
+
if (colonIndex < str.length - 1 && str[colonIndex + 1] !== ' ' && str[colonIndex + 1] !== '\t') {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const key = str.slice(0, colonIndex).trim();
|
|
120
|
+
const value = str.slice(colonIndex + 1).trim();
|
|
121
|
+
return { key, value };
|
|
122
|
+
}
|
|
123
|
+
|
|
84
124
|
/**
|
|
85
125
|
* Extract YAML frontmatter from markdown if present.
|
|
86
126
|
* Returns the parsed frontmatter, the remaining markdown, and the 1-based
|
|
@@ -112,57 +152,127 @@ export function extractFrontmatter(markdown: string): { frontmatter: Frontmatter
|
|
|
112
152
|
const consumedTotal = leadingChars + consumedInTrimmed;
|
|
113
153
|
const contentStartLine = (markdown.slice(0, consumedTotal).match(/\n/g) || []).length + 1;
|
|
114
154
|
|
|
115
|
-
// Parse simple YAML (key: value pairs)
|
|
155
|
+
// Parse simple YAML (key: value pairs, indentation-aware)
|
|
116
156
|
const frontmatter: Frontmatter = {};
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
const mapStack: { indent: number; map: { [key: string]: FrontmatterValue } }[] = [
|
|
158
|
+
{ indent: -1, map: frontmatter },
|
|
159
|
+
];
|
|
160
|
+
const arrayStack: { indent: number; array: FrontmatterValue[] }[] = [];
|
|
161
|
+
let pendingKey: {
|
|
162
|
+
key: string;
|
|
163
|
+
indent: number;
|
|
164
|
+
parentMap: { [key: string]: FrontmatterValue };
|
|
165
|
+
} | null = null;
|
|
119
166
|
|
|
120
167
|
const lines = frontmatterRaw.split('\n');
|
|
121
168
|
for (let i = 0; i < lines.length; i++) {
|
|
122
|
-
const rawLine = lines[i];
|
|
169
|
+
const rawLine = lines[i].replace(/\r$/, '');
|
|
123
170
|
const trimmedLine = rawLine.trim();
|
|
124
171
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
172
|
+
if (!trimmedLine) continue;
|
|
173
|
+
|
|
174
|
+
const lineIndent = indentWidth(rawLine);
|
|
175
|
+
|
|
176
|
+
// Array item (- value or - key: value)
|
|
177
|
+
if (trimmedLine.startsWith('- ')) {
|
|
178
|
+
const afterDash = trimmedLine.slice(2).trim();
|
|
179
|
+
const kv = parseKeyValue(afterDash);
|
|
180
|
+
|
|
181
|
+
if (pendingKey && lineIndent >= pendingKey.indent) {
|
|
182
|
+
const newArray: FrontmatterValue[] = [];
|
|
183
|
+
pendingKey.parentMap[pendingKey.key] = newArray;
|
|
184
|
+
arrayStack.push({ indent: lineIndent, array: newArray });
|
|
185
|
+
pendingKey = null;
|
|
186
|
+
} else {
|
|
187
|
+
pendingKey = null;
|
|
188
|
+
while (arrayStack.length > 0 && arrayStack[arrayStack.length - 1].indent > lineIndent) {
|
|
189
|
+
arrayStack.pop();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
while (mapStack.length > 1 && mapStack[mapStack.length - 1].indent >= lineIndent) {
|
|
194
|
+
mapStack.pop();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const targetArray = arrayStack.length > 0 ? arrayStack[arrayStack.length - 1].array : null;
|
|
198
|
+
|
|
199
|
+
if (kv) {
|
|
200
|
+
const blockScalar = kv.value.match(/^([|>])[+-]?$/);
|
|
201
|
+
let scalarVal = kv.value;
|
|
202
|
+
if (blockScalar) {
|
|
203
|
+
const { value: parsedScalar, endIndex } = parseBlockScalar(
|
|
204
|
+
lines,
|
|
205
|
+
i + 1,
|
|
206
|
+
lineIndent,
|
|
207
|
+
blockScalar[1] === '>',
|
|
208
|
+
);
|
|
209
|
+
scalarVal = parsedScalar;
|
|
210
|
+
i = endIndex;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const mapElem: { [key: string]: FrontmatterValue } = {};
|
|
214
|
+
if (scalarVal) {
|
|
215
|
+
mapElem[kv.key] = scalarVal;
|
|
216
|
+
} else {
|
|
217
|
+
// The key sits two columns right of the dash (`- meta:`), so that —
|
|
218
|
+
// not the dash's own indent — is the indent its nested block must be
|
|
219
|
+
// measured against. Recording `lineIndent` here made the nested map
|
|
220
|
+
// swallow the item's later sibling keys, because a sibling indented
|
|
221
|
+
// to the key's column never dedented past the dash.
|
|
222
|
+
pendingKey = { key: kv.key, indent: lineIndent + 2, parentMap: mapElem };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (targetArray) {
|
|
226
|
+
targetArray.push(mapElem);
|
|
227
|
+
}
|
|
228
|
+
mapStack.push({ indent: lineIndent, map: mapElem });
|
|
229
|
+
} else {
|
|
230
|
+
if (targetArray) {
|
|
231
|
+
targetArray.push(afterDash);
|
|
232
|
+
}
|
|
131
233
|
}
|
|
132
|
-
currentArray.push(value);
|
|
133
234
|
continue;
|
|
134
235
|
}
|
|
135
236
|
|
|
136
237
|
// Key: value pair
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
238
|
+
const kv = parseKeyValue(trimmedLine);
|
|
239
|
+
if (kv) {
|
|
240
|
+
if (pendingKey) {
|
|
241
|
+
if (lineIndent > pendingKey.indent) {
|
|
242
|
+
const newMap: { [key: string]: FrontmatterValue } = {};
|
|
243
|
+
pendingKey.parentMap[pendingKey.key] = newMap;
|
|
244
|
+
mapStack.push({ indent: pendingKey.indent, map: newMap });
|
|
245
|
+
}
|
|
246
|
+
pendingKey = null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
while (arrayStack.length > 0 && arrayStack[arrayStack.length - 1].indent >= lineIndent) {
|
|
250
|
+
arrayStack.pop();
|
|
251
|
+
}
|
|
252
|
+
while (mapStack.length > 1 && mapStack[mapStack.length - 1].indent >= lineIndent) {
|
|
253
|
+
mapStack.pop();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const parentMap = mapStack[mapStack.length - 1].map;
|
|
257
|
+
|
|
258
|
+
// Block scalar: `|` or `>`
|
|
259
|
+
const blockScalar = kv.value.match(/^([|>])[+-]?$/);
|
|
152
260
|
if (blockScalar) {
|
|
153
261
|
const { value: scalarValue, endIndex } = parseBlockScalar(
|
|
154
262
|
lines,
|
|
155
263
|
i + 1,
|
|
156
|
-
|
|
264
|
+
lineIndent,
|
|
157
265
|
blockScalar[1] === '>',
|
|
158
266
|
);
|
|
159
|
-
|
|
267
|
+
parentMap[kv.key] = scalarValue;
|
|
160
268
|
i = endIndex;
|
|
161
269
|
continue;
|
|
162
270
|
}
|
|
163
271
|
|
|
164
|
-
if (value) {
|
|
165
|
-
|
|
272
|
+
if (kv.value) {
|
|
273
|
+
parentMap[kv.key] = kv.value;
|
|
274
|
+
} else {
|
|
275
|
+
pendingKey = { key: kv.key, indent: lineIndent, parentMap };
|
|
166
276
|
}
|
|
167
277
|
}
|
|
168
278
|
}
|
|
@@ -53,8 +53,8 @@ export interface TerminalToolsAnnouncementGateState {
|
|
|
53
53
|
/**
|
|
54
54
|
* Chain gate for the announcement. It is LAST in each app's first-run dialog
|
|
55
55
|
* chain, after every dialog that asks the user to decide something (code
|
|
56
|
-
* review: guide intro, look-and-feel,
|
|
57
|
-
*
|
|
56
|
+
* review: guide intro, look-and-feel, edit mode, token hover cards; plan and
|
|
57
|
+
* annotate: look-and-feel, goal setup, permission mode).
|
|
58
58
|
*
|
|
59
59
|
* Last rather than first because none of those dialogs consume this cookie:
|
|
60
60
|
* a session that is busy asking questions defers the announcement to the next
|
|
Binary file
|
package/assets/review-tree.png
DELETED
|
Binary file
|
package/utils/codexModels.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codex Model Catalog
|
|
3
|
-
*
|
|
4
|
-
* Single source of truth for the Codex models offered by the launch panels
|
|
5
|
-
* (AgentsTab + GuideEmptyState) and their per-model reasoning efforts.
|
|
6
|
-
* Aligned with the Codex CLI's own model catalog (codex-cli 0.144): each
|
|
7
|
-
* entry carries the efforts that model actually accepts plus the CLI's
|
|
8
|
-
* default effort for it, so the UI never offers (or launches) an effort the
|
|
9
|
-
* model would reject. Lives in utils/ rather than AgentsTab so
|
|
10
|
-
* useAgentSettings can clamp saved efforts without importing a component.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export interface CodexModelOption {
|
|
14
|
-
value: string;
|
|
15
|
-
label: string;
|
|
16
|
-
/** Reasoning efforts this model supports, per the Codex CLI catalog. */
|
|
17
|
-
efforts: string[];
|
|
18
|
-
/** The CLI's default effort for this model — the clamp target when a saved
|
|
19
|
-
* effort isn't in `efforts`. */
|
|
20
|
-
defaultEffort: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// The two effort ladders in the current catalog. No model supports `minimal`
|
|
24
|
-
// anymore (saved picks migrate to `low` — see useAgentSettings).
|
|
25
|
-
const EFFORTS_THROUGH_XHIGH = ['low', 'medium', 'high', 'xhigh'];
|
|
26
|
-
const EFFORTS_THROUGH_MAX = [...EFFORTS_THROUGH_XHIGH, 'max'];
|
|
27
|
-
const EFFORTS_THROUGH_ULTRA = [...EFFORTS_THROUGH_MAX, 'ultra'];
|
|
28
|
-
|
|
29
|
-
export const CODEX_MODELS: CodexModelOption[] = [
|
|
30
|
-
// GPT-5.6 naming scheme: `-sol` is the flagship, `-terra` is the mid
|
|
31
|
-
// price/performance tier, and `-luna` is the efficient high-volume tier.
|
|
32
|
-
{ value: 'gpt-5.6-sol', label: 'GPT-5.6 Sol', efforts: EFFORTS_THROUGH_ULTRA, defaultEffort: 'low' },
|
|
33
|
-
{ value: 'gpt-5.6-terra', label: 'GPT-5.6 Terra', efforts: EFFORTS_THROUGH_ULTRA, defaultEffort: 'medium' },
|
|
34
|
-
{ value: 'gpt-5.6-luna', label: 'GPT-5.6 Luna', efforts: EFFORTS_THROUGH_MAX, defaultEffort: 'medium' },
|
|
35
|
-
{ value: 'gpt-5.5', label: 'GPT-5.5', efforts: EFFORTS_THROUGH_XHIGH, defaultEffort: 'medium' },
|
|
36
|
-
{ value: 'gpt-5.4', label: 'GPT-5.4', efforts: EFFORTS_THROUGH_XHIGH, defaultEffort: 'medium' },
|
|
37
|
-
{ value: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark', efforts: EFFORTS_THROUGH_XHIGH, defaultEffort: 'high' },
|
|
38
|
-
// gpt-5.2 is retained: it was retired from the ChatGPT product (steered to
|
|
39
|
-
// 5.5) but the API still serves it, so API-key Codex users keep it. The
|
|
40
|
-
// rest of the 5.2/5.1 family (gpt-5.2-codex, gpt-5.1-codex-max,
|
|
41
|
-
// gpt-5.1-codex-mini — and gpt-5.3-codex before them) is API-shut-down per
|
|
42
|
-
// OpenAI's deprecations page (2026-07-23), dead for ALL auth modes; saved
|
|
43
|
-
// picks migrate in useAgentSettings. gpt-5.2 predates max/ultra, so it
|
|
44
|
-
// gets the safe historical effort set.
|
|
45
|
-
{ value: 'gpt-5.2', label: 'GPT-5.2', efforts: EFFORTS_THROUGH_XHIGH, defaultEffort: 'medium' },
|
|
46
|
-
{ value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', efforts: EFFORTS_THROUGH_XHIGH, defaultEffort: 'medium' },
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
/** Display labels for the reasoning-effort ids across every model. */
|
|
50
|
-
export const CODEX_EFFORT_LABELS: Record<string, string> = {
|
|
51
|
-
low: 'Low',
|
|
52
|
-
medium: 'Medium',
|
|
53
|
-
high: 'High',
|
|
54
|
-
xhigh: 'XHigh',
|
|
55
|
-
max: 'Max',
|
|
56
|
-
ultra: 'Ultra',
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Fallback effort set for a model we don't know (a saved pick of a future
|
|
60
|
-
// model id passes through migration untouched, so the picker still needs
|
|
61
|
-
// SOMETHING to offer). low..xhigh is supported by every catalog model.
|
|
62
|
-
const UNKNOWN_MODEL_EFFORTS = EFFORTS_THROUGH_XHIGH;
|
|
63
|
-
|
|
64
|
-
/** The reasoning-effort picker options for one model — only the efforts that
|
|
65
|
-
* model actually supports. */
|
|
66
|
-
export function codexReasoningOptions(model: string): Array<{ value: string; label: string }> {
|
|
67
|
-
const entry = CODEX_MODELS.find((m) => m.value === model);
|
|
68
|
-
const efforts = entry?.efforts ?? UNKNOWN_MODEL_EFFORTS;
|
|
69
|
-
return efforts.map((value) => ({ value, label: CODEX_EFFORT_LABELS[value] ?? value }));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Clamp a saved reasoning effort to what the model supports: an unsupported
|
|
73
|
-
* effort snaps to the model's catalog default effort. Unknown models pass
|
|
74
|
-
* through unchanged (we can't know their supported set). */
|
|
75
|
-
export function clampCodexReasoning(model: string, reasoning: string): string {
|
|
76
|
-
const entry = CODEX_MODELS.find((m) => m.value === model);
|
|
77
|
-
if (!entry) return reasoning;
|
|
78
|
-
return entry.efforts.includes(reasoning) ? reasoning : entry.defaultEffort;
|
|
79
|
-
}
|