@sagmans/dsh-tui 0.2.0 → 0.3.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/README.md +126 -21
- package/lib/agent/model.d.ts +26 -0
- package/lib/agent/model.d.ts.map +1 -1
- package/lib/agent/model.js +41 -0
- package/lib/agent/model.js.map +1 -1
- package/lib/agent/status.d.ts +10 -0
- package/lib/agent/status.d.ts.map +1 -1
- package/lib/agent/status.js +2 -0
- package/lib/agent/status.js.map +1 -1
- package/lib/cards.d.ts +1 -1
- package/lib/cards.d.ts.map +1 -1
- package/lib/cards.js +4 -3
- package/lib/cards.js.map +1 -1
- package/lib/gates.d.ts +145 -2
- package/lib/gates.d.ts.map +1 -1
- package/lib/gates.js +335 -40
- package/lib/gates.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +383 -102
- package/lib/index.js.map +1 -1
- package/lib/input/actions.d.ts +136 -0
- package/lib/input/actions.d.ts.map +1 -0
- package/lib/input/actions.js +740 -0
- package/lib/input/actions.js.map +1 -0
- package/lib/input/keymap-settings.d.ts +21 -0
- package/lib/input/keymap-settings.d.ts.map +1 -0
- package/lib/input/keymap-settings.js +28 -0
- package/lib/input/keymap-settings.js.map +1 -0
- package/lib/input/keymap.d.ts +91 -0
- package/lib/input/keymap.d.ts.map +1 -0
- package/lib/input/keymap.js +170 -0
- package/lib/input/keymap.js.map +1 -0
- package/lib/input/match.d.ts +10 -0
- package/lib/input/match.d.ts.map +1 -0
- package/lib/input/match.js +83 -0
- package/lib/input/match.js.map +1 -0
- package/lib/input/submission.d.ts +6 -1
- package/lib/input/submission.d.ts.map +1 -1
- package/lib/input/submission.js +6 -2
- package/lib/input/submission.js.map +1 -1
- package/lib/input.d.ts +17 -0
- package/lib/input.d.ts.map +1 -0
- package/lib/input.js +27 -0
- package/lib/input.js.map +1 -0
- package/lib/keys-command.d.ts +16 -0
- package/lib/keys-command.d.ts.map +1 -0
- package/lib/keys-command.js +60 -0
- package/lib/keys-command.js.map +1 -0
- package/lib/queue.d.ts +6 -0
- package/lib/queue.d.ts.map +1 -0
- package/lib/queue.js +68 -0
- package/lib/queue.js.map +1 -0
- package/lib/settings-notice.d.ts +19 -0
- package/lib/settings-notice.d.ts.map +1 -0
- package/lib/settings-notice.js +30 -0
- package/lib/settings-notice.js.map +1 -0
- package/lib/theme-settings.d.ts +43 -8
- package/lib/theme-settings.d.ts.map +1 -1
- package/lib/theme-settings.js +66 -3
- package/lib/theme-settings.js.map +1 -1
- package/lib/theme-tokens.d.ts +4 -4
- package/lib/theme-tokens.d.ts.map +1 -1
- package/lib/theme-tokens.js +31 -5
- package/lib/theme-tokens.js.map +1 -1
- package/lib/ui/editor.d.ts +18 -1
- package/lib/ui/editor.d.ts.map +1 -1
- package/lib/ui/editor.js +80 -19
- package/lib/ui/editor.js.map +1 -1
- package/lib/ui/frame.d.ts +54 -0
- package/lib/ui/frame.d.ts.map +1 -0
- package/lib/ui/frame.js +79 -0
- package/lib/ui/frame.js.map +1 -0
- package/lib/ui/gate-input.d.ts +20 -0
- package/lib/ui/gate-input.d.ts.map +1 -0
- package/lib/ui/gate-input.js +96 -0
- package/lib/ui/gate-input.js.map +1 -0
- package/lib/ui/markdown.d.ts +11 -2
- package/lib/ui/markdown.d.ts.map +1 -1
- package/lib/ui/markdown.js +25 -6
- package/lib/ui/markdown.js.map +1 -1
- package/lib/ui/mermaid.d.ts +44 -0
- package/lib/ui/mermaid.d.ts.map +1 -0
- package/lib/ui/mermaid.js +178 -0
- package/lib/ui/mermaid.js.map +1 -0
- package/lib/ui/picker.d.ts +30 -8
- package/lib/ui/picker.d.ts.map +1 -1
- package/lib/ui/picker.js +63 -23
- package/lib/ui/picker.js.map +1 -1
- package/lib/ui/prompt.d.ts +30 -0
- package/lib/ui/prompt.d.ts.map +1 -0
- package/lib/ui/prompt.js +45 -0
- package/lib/ui/prompt.js.map +1 -0
- package/lib/ui/queue.d.ts +24 -0
- package/lib/ui/queue.d.ts.map +1 -0
- package/lib/ui/queue.js +57 -0
- package/lib/ui/queue.js.map +1 -0
- package/lib/ui/status.d.ts +4 -0
- package/lib/ui/status.d.ts.map +1 -1
- package/lib/ui/status.js +8 -0
- package/lib/ui/status.js.map +1 -1
- package/lib/ui/view.d.ts +22 -2
- package/lib/ui/view.d.ts.map +1 -1
- package/lib/ui/view.js +84 -24
- package/lib/ui/view.js.map +1 -1
- package/lib/work.d.ts +48 -0
- package/lib/work.d.ts.map +1 -1
- package/lib/work.js +44 -0
- package/lib/work.js.map +1 -1
- package/package.json +3 -2
package/lib/ui/markdown.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MarkdownTheme } from '@earendil-works/pi-tui';
|
|
2
|
+
import type { MermaidTransform } from './mermaid.ts';
|
|
2
3
|
/** Parsed messages kept for redraws; everything above this is cold transcript. */
|
|
3
4
|
export declare const MARKDOWN_CACHE_LIMIT = 64;
|
|
4
5
|
/**
|
|
@@ -10,9 +11,17 @@ export declare const MARKDOWN_CACHE_LIMIT = 64;
|
|
|
10
11
|
*/
|
|
11
12
|
export declare class MarkdownRenderer {
|
|
12
13
|
private readonly theme;
|
|
14
|
+
private readonly mermaid?;
|
|
13
15
|
private readonly parsed;
|
|
14
|
-
constructor(theme: MarkdownTheme);
|
|
15
|
-
render(text: string, width: number): string[];
|
|
16
|
+
constructor(theme: MarkdownTheme, mermaid?: MermaidTransform | undefined);
|
|
17
|
+
render(text: string, width: number, live?: boolean): string[];
|
|
18
|
+
/**
|
|
19
|
+
* The parse options this renderer installs, absent when nothing transforms.
|
|
20
|
+
*
|
|
21
|
+
* The flag is captured rather than read per call: the component caches the
|
|
22
|
+
* rows it drew, so the value that built it is the only one it could honour.
|
|
23
|
+
*/
|
|
24
|
+
private options;
|
|
16
25
|
/**
|
|
17
26
|
* Drop parsed messages after a theme change.
|
|
18
27
|
*
|
package/lib/ui/markdown.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/ui/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/ui/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEpD,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAGf,OAAO,CAAC,QAAQ,CAAC,KAAK;IAAiB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAF5E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;gBAExB,KAAK,EAAE,aAAa,EAAmB,OAAO,CAAC,EAAE,gBAAgB,YAAA;IAE9F,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,UAAQ,GAAG,MAAM,EAAE;IAqB3D;;;;;OAKG;IACH,OAAO,CAAC,OAAO;IAMf;;;;;;OAMG;IACH,UAAU,IAAI,IAAI;CAInB"}
|
package/lib/ui/markdown.js
CHANGED
|
@@ -10,27 +10,46 @@ export const MARKDOWN_CACHE_LIMIT = 64;
|
|
|
10
10
|
*/
|
|
11
11
|
export class MarkdownRenderer {
|
|
12
12
|
theme;
|
|
13
|
+
mermaid;
|
|
13
14
|
parsed = new Map();
|
|
14
|
-
constructor(theme) {
|
|
15
|
+
constructor(theme, mermaid) {
|
|
15
16
|
this.theme = theme;
|
|
17
|
+
this.mermaid = mermaid;
|
|
16
18
|
}
|
|
17
|
-
render(text, width) {
|
|
18
|
-
|
|
19
|
+
render(text, width, live = false) {
|
|
20
|
+
// A streaming reply can settle on the very text it last streamed, and the
|
|
21
|
+
// two renderings are not the same: only a settled one may report what a
|
|
22
|
+
// drawing lost, and a mode may draw one and withhold the other. The flag is
|
|
23
|
+
// part of a message's identity for the same reason the text is.
|
|
24
|
+
const key = live ? `live\u0000${text}` : `settled\u0000${text}`;
|
|
25
|
+
let message = this.parsed.get(key);
|
|
19
26
|
if (message === undefined) {
|
|
20
27
|
if (this.parsed.size >= MARKDOWN_CACHE_LIMIT) {
|
|
21
28
|
const oldest = this.parsed.keys().next();
|
|
22
29
|
if (oldest.done !== true)
|
|
23
30
|
this.parsed.delete(oldest.value);
|
|
24
31
|
}
|
|
25
|
-
message = new Markdown(text, 0, 0, this.theme);
|
|
32
|
+
message = new Markdown(text, 0, 0, this.theme, undefined, this.options(live));
|
|
26
33
|
}
|
|
27
34
|
else {
|
|
28
35
|
// Reinserting keeps a message that is still on screen from ageing out.
|
|
29
|
-
this.parsed.delete(
|
|
36
|
+
this.parsed.delete(key);
|
|
30
37
|
}
|
|
31
|
-
this.parsed.set(
|
|
38
|
+
this.parsed.set(key, message);
|
|
32
39
|
return message.render(Math.max(1, Math.floor(width)));
|
|
33
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The parse options this renderer installs, absent when nothing transforms.
|
|
43
|
+
*
|
|
44
|
+
* The flag is captured rather than read per call: the component caches the
|
|
45
|
+
* rows it drew, so the value that built it is the only one it could honour.
|
|
46
|
+
*/
|
|
47
|
+
options(live) {
|
|
48
|
+
const mermaid = this.mermaid;
|
|
49
|
+
if (mermaid === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
return { transform: (markdown, availableWidth) => mermaid(markdown, availableWidth, live) };
|
|
52
|
+
}
|
|
34
53
|
/**
|
|
35
54
|
* Drop parsed messages after a theme change.
|
|
36
55
|
*
|
package/lib/ui/markdown.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/ui/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/ui/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAA4C,MAAM,wBAAwB,CAAA;AAG3F,kFAAkF;AAClF,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAA;AAEtC;;;;;;GAMG;AACH,MAAM,OAAO,gBAAgB;IAGE;IAAuC;IAFnD,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAA;IAErD,YAA6B,KAAoB,EAAmB,OAA0B;QAAjE,UAAK,GAAL,KAAK,CAAe;QAAmB,YAAO,GAAP,OAAO,CAAmB;IAAG,CAAC;IAElG,MAAM,CAAC,IAAY,EAAE,KAAa,EAAE,IAAI,GAAG,KAAK;QAC9C,0EAA0E;QAC1E,wEAAwE;QACxE,4EAA4E;QAC5E,gEAAgE;QAChE,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC,gBAAgB,IAAI,EAAE,CAAA;QAC/D,IAAI,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,oBAAoB,EAAE,CAAC;gBAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAA;gBACxC,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI;oBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YAC5D,CAAC;YACD,OAAO,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;QAC/E,CAAC;aAAM,CAAC;YACN,uEAAuE;YACvE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACzB,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC7B,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACvD,CAAC;IAED;;;;;OAKG;IACK,OAAO,CAAC,IAAa;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;QAC5B,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,SAAS,CAAA;QAC3C,OAAO,EAAE,SAAS,EAAE,CAAC,QAAQ,EAAE,cAAc,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,CAAA;IAC7F,CAAC;IAED;;;;;;OAMG;IACH,UAAU;QACR,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;YAAE,OAAO,CAAC,UAAU,EAAE,CAAA;QAChE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAA;IACrB,CAAC;CACF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { MermaidMode } from '../theme-settings.ts';
|
|
2
|
+
import type { TuiTheme } from '../theme.ts';
|
|
3
|
+
/** Drawings kept before the oldest is laid out again; a streaming reply redraws per delta. */
|
|
4
|
+
export declare const MERMAID_CACHE_LIMIT = 32;
|
|
5
|
+
/**
|
|
6
|
+
* The largest source this surface lays out.
|
|
7
|
+
*
|
|
8
|
+
* Layout is synchronous and happens inside a frame, and a streaming reply asks
|
|
9
|
+
* for it again on every delta, so past this the frame costs more than the
|
|
10
|
+
* drawing is worth and the source stays. Well above what the renderer's own
|
|
11
|
+
* node cap can fill, so no drawing a reader actually sees is kept out.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MERMAID_MAX_SOURCE: number;
|
|
14
|
+
export interface MermaidOptions {
|
|
15
|
+
readonly theme: TuiTheme;
|
|
16
|
+
/** Read per render, so a settings edit reaches a session that is already on screen. */
|
|
17
|
+
readonly mode: () => MermaidMode;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Rewrite one message's markdown, with `live` telling a streaming reply from a settled one.
|
|
21
|
+
*
|
|
22
|
+
* The width is the columns the message actually has, and a drawing that needs
|
|
23
|
+
* more than that is left as source rather than truncated: a mutilated diagram
|
|
24
|
+
* is worse than the text a reader can still read.
|
|
25
|
+
*/
|
|
26
|
+
export type MermaidTransform = (markdown: string, availableWidth: number, live: boolean) => string;
|
|
27
|
+
/**
|
|
28
|
+
* Draw a reply's mermaid fences as terminal art.
|
|
29
|
+
*
|
|
30
|
+
* Replacements are spliced into the message at the offsets the lexer reported,
|
|
31
|
+
* never rebuilt from the tokens: lexing can decide something is not worth a
|
|
32
|
+
* token — a duplicate reference definition, for one — and a message that merely
|
|
33
|
+
* mentions mermaid would then change under a reader who asked for nothing. Text
|
|
34
|
+
* the transform does not draw is copied through byte for byte.
|
|
35
|
+
*
|
|
36
|
+
* A fence is replaced before the markdown is parsed, because that is the only
|
|
37
|
+
* moment the source is still whole: once tokens are flowing the drawing's rows
|
|
38
|
+
* would be re-wrapped as prose and lose the spacing they are made of. Each row
|
|
39
|
+
* is therefore emitted as an inline code span, which the markdown renderer
|
|
40
|
+
* keeps verbatim, and rows are separated by hard breaks so a diagram survives
|
|
41
|
+
* as one block instead of one paragraph.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createMermaidTransform(options: MermaidOptions): MermaidTransform;
|
|
44
|
+
//# sourceMappingURL=mermaid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mermaid.d.ts","sourceRoot":"","sources":["../../src/ui/mermaid.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAEvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAQ3C,8FAA8F;AAC9F,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,QAAY,CAAA;AA8B3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAA;IACxB,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,WAAW,CAAA;CACjC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,MAAM,CAAA;AAgElG;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,cAAc,GAAG,gBAAgB,CAkEhF"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Marked, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
|
+
import { render } from 'lovely-mermaid';
|
|
3
|
+
/** The one fence language that asks for a drawing. */
|
|
4
|
+
const MERMAID_FENCE_LANG = 'mermaid';
|
|
5
|
+
/** The cheap guard before lexing; case-insensitive because a fence may shout. */
|
|
6
|
+
const MERMAID_MENTION = /mermaid/iu;
|
|
7
|
+
/** Drawings kept before the oldest is laid out again; a streaming reply redraws per delta. */
|
|
8
|
+
export const MERMAID_CACHE_LIMIT = 32;
|
|
9
|
+
/**
|
|
10
|
+
* The largest source this surface lays out.
|
|
11
|
+
*
|
|
12
|
+
* Layout is synchronous and happens inside a frame, and a streaming reply asks
|
|
13
|
+
* for it again on every delta, so past this the frame costs more than the
|
|
14
|
+
* drawing is worth and the source stays. Well above what the renderer's own
|
|
15
|
+
* node cap can fill, so no drawing a reader actually sees is kept out.
|
|
16
|
+
*/
|
|
17
|
+
export const MERMAID_MAX_SOURCE = 32 * 1024;
|
|
18
|
+
/** What ends a row without starting a paragraph; two trailing spaces are CommonMark's hard break. */
|
|
19
|
+
const HARD_BREAK = ' \n';
|
|
20
|
+
/** An empty code span has no height, so a blank row stands on a non-breaking space. */
|
|
21
|
+
const BLANK_ROW = '\u00a0';
|
|
22
|
+
const WARNING_LEAD = 'Mermaid diagram not rendered: ';
|
|
23
|
+
const WARNING_MORE = ' (+';
|
|
24
|
+
/** A backtick run inside a row needs a longer delimiter, and a row may hold one inside a label. */
|
|
25
|
+
const BACKTICK_RUN = /`+/gu;
|
|
26
|
+
/**
|
|
27
|
+
* Which element each run of a drawing is, as the token that draws it.
|
|
28
|
+
*
|
|
29
|
+
* The renderer reports what a run *is* and never a colour, so this table is the
|
|
30
|
+
* only place that relates a diagram to the theme; everything else works with
|
|
31
|
+
* roles. Author-assigned classes are deliberately not consulted: a reader's
|
|
32
|
+
* theme is the surface's, not the diagram's.
|
|
33
|
+
*/
|
|
34
|
+
const ROLE_TOKEN = {
|
|
35
|
+
border: 'markdown.diagram.border',
|
|
36
|
+
text: 'markdown.diagram.text',
|
|
37
|
+
edge: 'markdown.diagram.edge',
|
|
38
|
+
edgeLabel: 'markdown.diagram.edgeLabel',
|
|
39
|
+
title: 'markdown.diagram.title',
|
|
40
|
+
};
|
|
41
|
+
function isMermaidFence(token) {
|
|
42
|
+
// The info string may carry more than the language, and a fence may shout.
|
|
43
|
+
return token.type === 'code' && token.lang?.trim().split(/\s+/, 1)[0]?.toLowerCase() === MERMAID_FENCE_LANG;
|
|
44
|
+
}
|
|
45
|
+
/** One drawing row as the code span that keeps its spacing. */
|
|
46
|
+
function codeSpan(row) {
|
|
47
|
+
const content = row === '' ? BLANK_ROW : row;
|
|
48
|
+
let longest = 0;
|
|
49
|
+
// Iterated, not spread: a row can carry more runs than a call holds arguments.
|
|
50
|
+
for (const run of content.matchAll(BACKTICK_RUN)) {
|
|
51
|
+
if (run[0].length > longest)
|
|
52
|
+
longest = run[0].length;
|
|
53
|
+
}
|
|
54
|
+
const fence = '`'.repeat(longest + 1);
|
|
55
|
+
// A row that starts or ends with a backtick would otherwise be read as the delimiter itself.
|
|
56
|
+
const padding = content.startsWith('`') || content.endsWith('`') ? ' ' : '';
|
|
57
|
+
return `${fence}${padding}${content}${padding}${fence}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The columns a drawing really occupies.
|
|
61
|
+
*
|
|
62
|
+
* The renderer measures with its own tables, and they are not always the
|
|
63
|
+
* terminal's: a halfwidth kana plus a combining mark counts one column there
|
|
64
|
+
* and two on screen. The rows are what the reader gets, so the rows decide.
|
|
65
|
+
*/
|
|
66
|
+
function drawnWidth(art) {
|
|
67
|
+
let widest = 0;
|
|
68
|
+
for (const row of art.plain) {
|
|
69
|
+
const columns = visibleWidth(row);
|
|
70
|
+
if (columns > widest)
|
|
71
|
+
widest = columns;
|
|
72
|
+
}
|
|
73
|
+
return widest;
|
|
74
|
+
}
|
|
75
|
+
function warningText(art) {
|
|
76
|
+
const first = art.warnings[0] ?? '';
|
|
77
|
+
const rest = art.warnings.length - 1;
|
|
78
|
+
return `${WARNING_LEAD}${first}${rest > 0 ? `${WARNING_MORE}${rest} more)` : ''}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Lay out each distinct source once.
|
|
82
|
+
*
|
|
83
|
+
* A streaming reply asks for the same fences again on every delta, and laying
|
|
84
|
+
* out a diagram is the expensive half of drawing it; the other half is styling
|
|
85
|
+
* runs, which stays per call so a theme swap needs no invalidation.
|
|
86
|
+
*/
|
|
87
|
+
function artCache() {
|
|
88
|
+
const drawn = new Map();
|
|
89
|
+
return source => {
|
|
90
|
+
if (drawn.has(source))
|
|
91
|
+
return drawn.get(source) ?? null;
|
|
92
|
+
const art = render(source);
|
|
93
|
+
if (drawn.size >= MERMAID_CACHE_LIMIT) {
|
|
94
|
+
const oldest = drawn.keys().next();
|
|
95
|
+
if (oldest.done !== true)
|
|
96
|
+
drawn.delete(oldest.value);
|
|
97
|
+
}
|
|
98
|
+
drawn.set(source, art);
|
|
99
|
+
return art;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Draw a reply's mermaid fences as terminal art.
|
|
104
|
+
*
|
|
105
|
+
* Replacements are spliced into the message at the offsets the lexer reported,
|
|
106
|
+
* never rebuilt from the tokens: lexing can decide something is not worth a
|
|
107
|
+
* token — a duplicate reference definition, for one — and a message that merely
|
|
108
|
+
* mentions mermaid would then change under a reader who asked for nothing. Text
|
|
109
|
+
* the transform does not draw is copied through byte for byte.
|
|
110
|
+
*
|
|
111
|
+
* A fence is replaced before the markdown is parsed, because that is the only
|
|
112
|
+
* moment the source is still whole: once tokens are flowing the drawing's rows
|
|
113
|
+
* would be re-wrapped as prose and lose the spacing they are made of. Each row
|
|
114
|
+
* is therefore emitted as an inline code span, which the markdown renderer
|
|
115
|
+
* keeps verbatim, and rows are separated by hard breaks so a diagram survives
|
|
116
|
+
* as one block instead of one paragraph.
|
|
117
|
+
*/
|
|
118
|
+
export function createMermaidTransform(options) {
|
|
119
|
+
const parser = new Marked();
|
|
120
|
+
const draw = artCache();
|
|
121
|
+
/**
|
|
122
|
+
* The block a body becomes, held apart from the text on either side.
|
|
123
|
+
*
|
|
124
|
+
* Once drawn, the rows are a paragraph, so a blank line on each side is what
|
|
125
|
+
* stops them joining the prose before them or turning a `---` after them into
|
|
126
|
+
* a heading underline. Source that was not drawn is already a block of its own
|
|
127
|
+
* and is left exactly as written.
|
|
128
|
+
*/
|
|
129
|
+
function isolated(body, before, after) {
|
|
130
|
+
const lead = before === '' || before.endsWith('\n\n') ? '' : '\n';
|
|
131
|
+
return `${lead}${body}${after === '' ? '\n' : '\n\n'}`;
|
|
132
|
+
}
|
|
133
|
+
/** What one fence becomes: art, or its source with the reason it stayed source. */
|
|
134
|
+
function fenceBlock(token, before, after, availableWidth, live) {
|
|
135
|
+
if (token.text.length > MERMAID_MAX_SOURCE)
|
|
136
|
+
return token.raw;
|
|
137
|
+
const art = draw(token.text);
|
|
138
|
+
// A settled drawing that lost part of its source is worth reporting whether
|
|
139
|
+
// or not it would have fit, because the reader is looking at the source.
|
|
140
|
+
if (!live && art !== null && art.warnings.length > 0) {
|
|
141
|
+
const reason = options.theme.style('markdown.diagram.warning', warningText(art));
|
|
142
|
+
return isolated(`${token.raw}\n${codeSpan(reason)}`, before, after);
|
|
143
|
+
}
|
|
144
|
+
if (art === null || drawnWidth(art) > availableWidth)
|
|
145
|
+
return token.raw;
|
|
146
|
+
const rows = art.styled.map(row => row
|
|
147
|
+
.map(span => (span.role === 'none' ? span.text : options.theme.style(ROLE_TOKEN[span.role], span.text)))
|
|
148
|
+
.join(''));
|
|
149
|
+
return isolated(rows.map(codeSpan).join(HARD_BREAK), before, after);
|
|
150
|
+
}
|
|
151
|
+
return (markdown, availableWidth, live) => {
|
|
152
|
+
const mode = options.mode();
|
|
153
|
+
// A reply that is still arriving is only drawn in streaming mode; the other
|
|
154
|
+
// modes wait for the settled text, which is also the only text whose losses
|
|
155
|
+
// are worth reporting.
|
|
156
|
+
if (mode === 'off' || (live && mode !== 'streaming'))
|
|
157
|
+
return markdown;
|
|
158
|
+
// Lexing is the cost this guard avoids: no fence can be present without the word.
|
|
159
|
+
if (!MERMAID_MENTION.test(markdown))
|
|
160
|
+
return markdown;
|
|
161
|
+
let out = '';
|
|
162
|
+
let cursor = 0;
|
|
163
|
+
for (const token of parser.lexer(markdown)) {
|
|
164
|
+
const at = markdown.indexOf(token.raw, cursor);
|
|
165
|
+
// A lexer that rewrote its own input cannot be spliced back into it, and
|
|
166
|
+
// the message is worth more than the drawing: it stays as written.
|
|
167
|
+
if (at < 0)
|
|
168
|
+
return markdown;
|
|
169
|
+
const body = isMermaidFence(token)
|
|
170
|
+
? fenceBlock(token, out, markdown.slice(at + token.raw.length), availableWidth, live)
|
|
171
|
+
: token.raw;
|
|
172
|
+
out += markdown.slice(cursor, at) + body;
|
|
173
|
+
cursor = at + token.raw.length;
|
|
174
|
+
}
|
|
175
|
+
return out + markdown.slice(cursor);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=mermaid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../../src/ui/mermaid.ts"],"names":[],"mappings":"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"}
|
package/lib/ui/picker.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type Keymap } from '../input/actions.ts';
|
|
1
2
|
import type { StoredSession } from '../agent/history.ts';
|
|
3
|
+
import { type ModelChoice, type ModelRoute } from '../agent/model.ts';
|
|
2
4
|
import { type PresetSummary } from '../agent/presets.ts';
|
|
3
5
|
/** One selectable row of the picker. */
|
|
4
6
|
export interface PickerRow {
|
|
@@ -34,10 +36,15 @@ export type PickerAction = {
|
|
|
34
36
|
* window stays around the cursor instead, and the counts say what is hidden.
|
|
35
37
|
*/
|
|
36
38
|
export declare const PICKER_WINDOW = 12;
|
|
37
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The hint lines a list uses when it is empty and when it holds rows.
|
|
41
|
+
*
|
|
42
|
+
* Read at paint time rather than built once, because a settings edit while the
|
|
43
|
+
* list is open moves the keys the hint names.
|
|
44
|
+
*/
|
|
38
45
|
export interface PickerHints {
|
|
39
|
-
readonly empty: string;
|
|
40
|
-
readonly listed: string;
|
|
46
|
+
readonly empty: () => string;
|
|
47
|
+
readonly listed: () => string;
|
|
41
48
|
}
|
|
42
49
|
/** Human age of a session, so a list of ids becomes a list of moments. */
|
|
43
50
|
export declare function describeAge(createdAt: number, now: number): string;
|
|
@@ -59,6 +66,8 @@ export declare class ListPicker<Row> {
|
|
|
59
66
|
/** Extra text the filter matches besides the row's own label. */
|
|
60
67
|
private readonly haystackOf;
|
|
61
68
|
private readonly hints;
|
|
69
|
+
/** The keys in force, read per press so a settings edit lands on the next key. */
|
|
70
|
+
private readonly keys;
|
|
62
71
|
private cursor;
|
|
63
72
|
private filter;
|
|
64
73
|
private note;
|
|
@@ -66,8 +75,10 @@ export declare class ListPicker<Row> {
|
|
|
66
75
|
/** The value a pick settles on. */
|
|
67
76
|
idOf: (row: Row) => string, describe: (row: Row) => PickerRow,
|
|
68
77
|
/** Extra text the filter matches besides the row's own label. */
|
|
69
|
-
haystackOf: (row: Row) => string, hints: PickerHints
|
|
70
|
-
/**
|
|
78
|
+
haystackOf: (row: Row) => string, hints: PickerHints,
|
|
79
|
+
/** The keys in force, read per press so a settings edit lands on the next key. */
|
|
80
|
+
keys: () => Keymap);
|
|
81
|
+
/** Rows matching the typed filter, best match first. */
|
|
71
82
|
visible(): readonly Row[];
|
|
72
83
|
/**
|
|
73
84
|
* Say why the row under the cursor cannot be taken.
|
|
@@ -89,7 +100,7 @@ export declare class ListPicker<Row> {
|
|
|
89
100
|
* first paint would make opening the picker as slow as the slowest session.
|
|
90
101
|
*/
|
|
91
102
|
export declare class SessionPicker extends ListPicker<StoredSession> {
|
|
92
|
-
constructor(sessions: readonly StoredSession[], titles: () => ReadonlyMap<string, string>, now
|
|
103
|
+
constructor(sessions: readonly StoredSession[], titles: () => ReadonlyMap<string, string>, now: (() => number) | undefined, keys: () => Keymap);
|
|
93
104
|
}
|
|
94
105
|
/**
|
|
95
106
|
* One agent preset chosen by key press.
|
|
@@ -98,7 +109,18 @@ export declare class SessionPicker extends ListPicker<StoredSession> {
|
|
|
98
109
|
* the picker is open is present or gone without reopening it.
|
|
99
110
|
*/
|
|
100
111
|
export declare class PresetPicker extends ListPicker<PresetSummary> {
|
|
101
|
-
constructor(presets: () => readonly PresetSummary[], currentId: () => string | undefined);
|
|
112
|
+
constructor(presets: () => readonly PresetSummary[], currentId: () => string | undefined, keys: () => Keymap);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* One configured model route chosen by key press.
|
|
116
|
+
*
|
|
117
|
+
* Rows are the caller's and are re-read on every key and paint, so a provider
|
|
118
|
+
* whose model list arrives after the picker opened appears in it without
|
|
119
|
+
* reopening, and the filter the reader already typed applies to those late
|
|
120
|
+
* rows too.
|
|
121
|
+
*/
|
|
122
|
+
export declare class ModelPicker extends ListPicker<ModelRoute> {
|
|
123
|
+
constructor(routes: () => readonly ModelRoute[], current: () => ModelChoice | undefined, keys: () => Keymap);
|
|
102
124
|
}
|
|
103
125
|
/**
|
|
104
126
|
* The pick id that clears an explicit effort.
|
|
@@ -135,6 +157,6 @@ export declare function effortChoices(efforts: readonly {
|
|
|
135
157
|
* cannot change while the list owns the keyboard.
|
|
136
158
|
*/
|
|
137
159
|
export declare class EffortPicker extends ListPicker<EffortChoice> {
|
|
138
|
-
constructor(choices: () => readonly EffortChoice[], heading: string);
|
|
160
|
+
constructor(choices: () => readonly EffortChoice[], heading: string, keys: () => Keymap);
|
|
139
161
|
}
|
|
140
162
|
//# sourceMappingURL=picker.d.ts.map
|
package/lib/ui/picker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/ui/picker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAExE,wCAAwC;AACxC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,6BAA6B;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,iDAAiD;AACjD,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAA;AAE/B;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,KAAK,CAAA;AAE/B
|
|
1
|
+
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/ui/picker.ts"],"names":[],"mappings":"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"}
|
package/lib/ui/picker.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { matchesKey } from '@earendil-works/pi-tui';
|
|
2
|
+
import { hintKeys, matchesAction, moveHint } from "../input/actions.js";
|
|
3
|
+
import { pastedText } from "../input.js";
|
|
4
|
+
import { matchScore } from "../input/match.js";
|
|
5
|
+
import { describeModelRoute, modelRouteKey } from "../agent/model.js";
|
|
2
6
|
import { describePreset } from "../agent/presets.js";
|
|
3
7
|
/**
|
|
4
8
|
* Rows the picker shows at once.
|
|
@@ -38,6 +42,7 @@ export class ListPicker {
|
|
|
38
42
|
describe;
|
|
39
43
|
haystackOf;
|
|
40
44
|
hints;
|
|
45
|
+
keys;
|
|
41
46
|
cursor = 0;
|
|
42
47
|
filter = '';
|
|
43
48
|
note;
|
|
@@ -45,20 +50,29 @@ export class ListPicker {
|
|
|
45
50
|
/** The value a pick settles on. */
|
|
46
51
|
idOf, describe,
|
|
47
52
|
/** Extra text the filter matches besides the row's own label. */
|
|
48
|
-
haystackOf, hints
|
|
53
|
+
haystackOf, hints,
|
|
54
|
+
/** The keys in force, read per press so a settings edit lands on the next key. */
|
|
55
|
+
keys) {
|
|
49
56
|
this.source = source;
|
|
50
57
|
this.heading = heading;
|
|
51
58
|
this.idOf = idOf;
|
|
52
59
|
this.describe = describe;
|
|
53
60
|
this.haystackOf = haystackOf;
|
|
54
61
|
this.hints = hints;
|
|
62
|
+
this.keys = keys;
|
|
55
63
|
}
|
|
56
|
-
/** Rows matching the typed filter,
|
|
64
|
+
/** Rows matching the typed filter, best match first. */
|
|
57
65
|
visible() {
|
|
58
|
-
const needle = this.filter.trim()
|
|
66
|
+
const needle = this.filter.trim();
|
|
59
67
|
if (needle === '')
|
|
60
68
|
return this.source();
|
|
61
|
-
|
|
69
|
+
// Ties keep the order the caller listed them in, so rows do not shuffle
|
|
70
|
+
// under the cursor while the reader is still typing.
|
|
71
|
+
return this.source()
|
|
72
|
+
.map(row => ({ row, score: matchScore(needle, this.haystackOf(row)) }))
|
|
73
|
+
.filter((entry) => entry.score !== undefined)
|
|
74
|
+
.sort((left, right) => right.score - left.score)
|
|
75
|
+
.map(entry => entry.row);
|
|
62
76
|
}
|
|
63
77
|
/**
|
|
64
78
|
* Say why the row under the cursor cannot be taken.
|
|
@@ -74,17 +88,20 @@ export class ListPicker {
|
|
|
74
88
|
handleKey(data) {
|
|
75
89
|
this.note = undefined;
|
|
76
90
|
const rows = this.visible();
|
|
77
|
-
|
|
91
|
+
// A picker owns the keyboard while it is open, so the interrupt key has to
|
|
92
|
+
// mean "leave this list" here: swallowing it would strand the reader.
|
|
93
|
+
const keys = this.keys();
|
|
94
|
+
if (matchesAction(keys, 'picker.cancel', data))
|
|
78
95
|
return { kind: 'cancel' };
|
|
79
|
-
if (
|
|
96
|
+
if (matchesAction(keys, 'picker.confirm', data)) {
|
|
80
97
|
const chosen = rows[Math.min(this.cursor, Math.max(0, rows.length - 1))];
|
|
81
98
|
return chosen === undefined ? undefined : { kind: 'pick', id: this.idOf(chosen) };
|
|
82
99
|
}
|
|
83
|
-
if (
|
|
100
|
+
if (matchesAction(keys, 'picker.up', data)) {
|
|
84
101
|
this.cursor = Math.max(0, this.cursor - 1);
|
|
85
102
|
return undefined;
|
|
86
103
|
}
|
|
87
|
-
if (
|
|
104
|
+
if (matchesAction(keys, 'picker.down', data)) {
|
|
88
105
|
this.cursor = Math.min(Math.max(0, rows.length - 1), this.cursor + 1);
|
|
89
106
|
return undefined;
|
|
90
107
|
}
|
|
@@ -98,8 +115,9 @@ export class ListPicker {
|
|
|
98
115
|
this.cursor = 0;
|
|
99
116
|
return undefined;
|
|
100
117
|
}
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
const text = pastedText(data) ?? (data.length === 1 && data >= ' ' ? data : undefined);
|
|
119
|
+
if (text !== undefined) {
|
|
120
|
+
this.filter += text;
|
|
103
121
|
this.cursor = 0;
|
|
104
122
|
}
|
|
105
123
|
return undefined;
|
|
@@ -119,7 +137,7 @@ export class ListPicker {
|
|
|
119
137
|
above: start,
|
|
120
138
|
below: Math.max(0, rows.length - start - window.length),
|
|
121
139
|
filter: this.filter,
|
|
122
|
-
hint: rows.length === 0 ? this.hints.empty : this.hints.listed,
|
|
140
|
+
hint: rows.length === 0 ? this.hints.empty() : this.hints.listed(),
|
|
123
141
|
};
|
|
124
142
|
}
|
|
125
143
|
}
|
|
@@ -131,7 +149,7 @@ export class ListPicker {
|
|
|
131
149
|
* first paint would make opening the picker as slow as the slowest session.
|
|
132
150
|
*/
|
|
133
151
|
export class SessionPicker extends ListPicker {
|
|
134
|
-
constructor(sessions, titles, now = () => Date.now()) {
|
|
152
|
+
constructor(sessions, titles, now = () => Date.now(), keys) {
|
|
135
153
|
const labelOf = (session) => titles().get(session.id) ?? session.id;
|
|
136
154
|
super(() => sessions, () => `resume a session · ${sessions.length} stored`, session => session.id, session => ({
|
|
137
155
|
label: labelOf(session),
|
|
@@ -143,9 +161,9 @@ export class SessionPicker extends ListPicker {
|
|
|
143
161
|
// The card marks the row under the cursor, which the list decides.
|
|
144
162
|
current: false,
|
|
145
163
|
}), session => [labelOf(session), session.id, session.cwd ?? ''].join(' '), {
|
|
146
|
-
empty:
|
|
147
|
-
listed: '
|
|
148
|
-
});
|
|
164
|
+
empty: () => `nothing matches · backspace to widen · ${hintKeys(keys(), 'picker.cancel')} cancel`,
|
|
165
|
+
listed: () => `${moveHint(keys(), 'picker.up', 'picker.down')} move · ${hintKeys(keys(), 'picker.confirm')} open · ${hintKeys(keys(), 'picker.cancel')} cancel · type to filter`,
|
|
166
|
+
}, keys);
|
|
149
167
|
}
|
|
150
168
|
}
|
|
151
169
|
/**
|
|
@@ -155,11 +173,33 @@ export class SessionPicker extends ListPicker {
|
|
|
155
173
|
* the picker is open is present or gone without reopening it.
|
|
156
174
|
*/
|
|
157
175
|
export class PresetPicker extends ListPicker {
|
|
158
|
-
constructor(presets, currentId) {
|
|
176
|
+
constructor(presets, currentId, keys) {
|
|
159
177
|
super(presets, () => `agent preset · ${presets().length} available`, preset => preset.id, preset => describePreset(preset, currentId()), preset => [preset.id, preset.name ?? '', preset.description ?? ''].join(' '), {
|
|
160
|
-
empty:
|
|
161
|
-
listed: '
|
|
162
|
-
});
|
|
178
|
+
empty: () => `nothing matches · backspace to widen · ${hintKeys(keys(), 'picker.cancel')} cancel`,
|
|
179
|
+
listed: () => `${moveHint(keys(), 'picker.up', 'picker.down')} move · ${hintKeys(keys(), 'picker.confirm')} switch · ${hintKeys(keys(), 'picker.cancel')} cancel · type to filter`,
|
|
180
|
+
}, keys);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* One configured model route chosen by key press.
|
|
185
|
+
*
|
|
186
|
+
* Rows are the caller's and are re-read on every key and paint, so a provider
|
|
187
|
+
* whose model list arrives after the picker opened appears in it without
|
|
188
|
+
* reopening, and the filter the reader already typed applies to those late
|
|
189
|
+
* rows too.
|
|
190
|
+
*/
|
|
191
|
+
export class ModelPicker extends ListPicker {
|
|
192
|
+
constructor(routes, current, keys) {
|
|
193
|
+
super(routes, () => {
|
|
194
|
+
const chosen = current();
|
|
195
|
+
if (chosen === undefined)
|
|
196
|
+
return 'model · no route in use';
|
|
197
|
+
const effort = chosen.reasoningEffort === undefined ? '' : ` (${chosen.reasoningEffort})`;
|
|
198
|
+
return `model · current ${chosen.provider}/${chosen.model}${effort}`;
|
|
199
|
+
}, route => modelRouteKey(route), route => describeModelRoute(route, current()), route => [route.provider, route.model, route.name].join(' '), {
|
|
200
|
+
empty: () => `nothing matched · /model <provider>/<model> takes any id · backspace to widen · ${hintKeys(keys(), 'picker.cancel')} cancel`,
|
|
201
|
+
listed: () => `${moveHint(keys(), 'picker.up', 'picker.down')} move · ${hintKeys(keys(), 'picker.confirm')} switch · ${hintKeys(keys(), 'picker.cancel')} cancel · type to filter`,
|
|
202
|
+
}, keys);
|
|
163
203
|
}
|
|
164
204
|
}
|
|
165
205
|
/**
|
|
@@ -200,7 +240,7 @@ export function effortChoices(efforts, effective) {
|
|
|
200
240
|
* cannot change while the list owns the keyboard.
|
|
201
241
|
*/
|
|
202
242
|
export class EffortPicker extends ListPicker {
|
|
203
|
-
constructor(choices, heading) {
|
|
243
|
+
constructor(choices, heading, keys) {
|
|
204
244
|
super(choices, () => heading, choice => choice.id, choice => ({
|
|
205
245
|
label: choice.name,
|
|
206
246
|
description: [
|
|
@@ -210,9 +250,9 @@ export class EffortPicker extends ListPicker {
|
|
|
210
250
|
].filter(part => part !== undefined && part !== '').join(' · ') || undefined,
|
|
211
251
|
current: choice.current,
|
|
212
252
|
}), choice => [choice.name, choice.id, choice.description ?? ''].join(' '), {
|
|
213
|
-
empty:
|
|
214
|
-
listed: '
|
|
215
|
-
});
|
|
253
|
+
empty: () => `nothing matches · backspace to widen · ${hintKeys(keys(), 'picker.cancel')} cancel`,
|
|
254
|
+
listed: () => `${moveHint(keys(), 'picker.up', 'picker.down')} move · ${hintKeys(keys(), 'picker.confirm')} apply · ${hintKeys(keys(), 'picker.cancel')} cancel · type to filter`,
|
|
255
|
+
}, keys);
|
|
216
256
|
}
|
|
217
257
|
}
|
|
218
258
|
//# sourceMappingURL=picker.js.map
|