@oh-my-pi/pi-tui 18.8.4 → 18.8.6
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 +23 -0
- package/dist/types/chat/user-message.d.ts +1 -1
- package/dist/types/overlays/model-browser.d.ts +13 -0
- package/dist/types/overlays/model-hub.d.ts +6 -0
- package/dist/types/overlays/session-selector.d.ts +5 -1
- package/dist/types/overlays/usage-dashboard.d.ts +5 -3
- package/dist/types/prompt/custom-editor.d.ts +2 -2
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/package.json +13 -9
- package/src/chat/assistant-message.ts +2 -2
- package/src/chat/user-message.ts +1 -1
- package/src/components/editor.ts +6 -3
- package/src/components/markdown.ts +182 -8
- package/src/overlays/model-browser.ts +26 -1
- package/src/overlays/model-hub.ts +172 -21
- package/src/overlays/session-selector.ts +11 -3
- package/src/overlays/usage-dashboard.ts +18 -7
- package/src/prompt/custom-editor.ts +3 -4
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/tips.txt +2 -1
- package/src/prompt/welcome.ts +46 -9
- package/src/theme/theme.ts +1 -0
- package/src/theme/tui-adapters.ts +68 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.8.6] - 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Improved rendering performance for streaming long Markdown lists while preserving nested items, numbering, and reference links.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Added package exports for the native Tern/TSP modules, allowing extensions in compiled `omp` binaries to import them.
|
|
14
|
+
- Fixed the Tern `/model` picker's Roles tab so typing and Backspace consistently edit the search field without triggering role commands.
|
|
15
|
+
- Fixed the session picker so sessions retain and display their directory when switching between the current folder and all-project views.
|
|
16
|
+
|
|
17
|
+
## [18.8.5] - 2026-10-08
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Added a **Compacts at** fact to the model hub preview and a Roles-view **Compaction limit** action (`k`) that edits the selected row's model limit through the new `ModelBrowserSource.compactionPointFor` and `ModelHubCallbacks.onCompactionPointChange` hooks ([#14952](https://github.com/can1357/oh-my-pi/pull/14952) by [@H4vC](https://github.com/H4vC))
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Fixed `/usage` dashboard cards reordering their limit rows by usage; rows now keep the provider's window order (e.g. 5 hour → weekly → monthly) ([#14953](https://github.com/can1357/oh-my-pi/pull/14953) by [@H4vC](https://github.com/H4vC))
|
|
26
|
+
- Fixed Cmd+A, Cmd+C, Cmd+X and Shift+arrow selection doing nothing in the Tern prompt while Vim mode is in Insert mode ([#14954](https://github.com/can1357/oh-my-pi/pull/14954) by [@H4vC](https://github.com/H4vC))
|
|
27
|
+
|
|
5
28
|
## [18.8.4] - 2026-10-08
|
|
6
29
|
|
|
7
30
|
### Changed
|
|
@@ -4,7 +4,7 @@ import type { ReactionTarget } from "./reaction.js";
|
|
|
4
4
|
import { type DescribeContext, type NativeNode, type NativeUiEvent } from "../native/node.js";
|
|
5
5
|
/** How a user bubble styles its prose and chips (see {@link userBubbleColor}). */
|
|
6
6
|
export interface UserBubbleOptions {
|
|
7
|
-
/**
|
|
7
|
+
/** Filesystem paths for attached image chips, indexed by chip number. */
|
|
8
8
|
imageLinks?: readonly (string | undefined)[];
|
|
9
9
|
/** The message's attached images in chip order (`#1` first); a native bubble shows them. */
|
|
10
10
|
images?: readonly ImageContent[];
|
|
@@ -37,6 +37,17 @@ export interface ResolvedModelRoleValue {
|
|
|
37
37
|
explicitThinkingLevel: boolean;
|
|
38
38
|
warning?: string;
|
|
39
39
|
}
|
|
40
|
+
/** Where auto-compaction triggers for one model, as the model hub's preview shows it (edited from the Roles view). */
|
|
41
|
+
export interface ModelCompactionPoint {
|
|
42
|
+
/** Context tokens that trigger auto-compaction; undefined when auto-compaction is off or the window is unknown. */
|
|
43
|
+
tokens: number | undefined;
|
|
44
|
+
/** The configured percentage of the window, when the trigger is percent-based. */
|
|
45
|
+
percent?: number;
|
|
46
|
+
/** What sets it: the matching `compaction.modelThresholds` key, `global`, or `default`. */
|
|
47
|
+
source: string;
|
|
48
|
+
/** The model's own exact entry as editable text (`90k`, `80%`); absent when it has none. */
|
|
49
|
+
draft?: string;
|
|
50
|
+
}
|
|
40
51
|
/** Host-provided preferences and model-role resolution for the browser. */
|
|
41
52
|
export interface ModelBrowserSource extends ModelRoleLookup {
|
|
42
53
|
/**
|
|
@@ -58,6 +69,8 @@ export interface ModelBrowserSource extends ModelRoleLookup {
|
|
|
58
69
|
getRoleInfo(role: string): ModelBrowserRoleInfo;
|
|
59
70
|
defaultRoleChain(role: string): string[];
|
|
60
71
|
resolveRoleValue(value: string | undefined, models: Model[], roleLookup?: ModelRoleLookup): ResolvedModelRoleValue;
|
|
72
|
+
/** Auto-compaction point for `model`; absent hosts show no compaction row in the preview. */
|
|
73
|
+
compactionPointFor?(model: Model): ModelCompactionPoint | undefined;
|
|
61
74
|
}
|
|
62
75
|
/** Read-only catalog surface consumed by model browsers. */
|
|
63
76
|
export interface ModelBrowserRegistry {
|
|
@@ -64,6 +64,12 @@ export interface ModelHubCallbacks {
|
|
|
64
64
|
onSwitchPreset?: (name: string) => void | Promise<void>;
|
|
65
65
|
/** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
|
|
66
66
|
onCycleOrderChange?: (order: string[]) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Persist typed text as `model`'s own compaction point: a token count (`90000`,
|
|
69
|
+
* `90k`, `1M`), a percentage (`80%`), or empty to reset. Returns an error
|
|
70
|
+
* message for input it rejects, which keeps the field open.
|
|
71
|
+
*/
|
|
72
|
+
onCompactionPointChange?: (model: Model, input: string) => string | undefined;
|
|
67
73
|
onCancel: () => void;
|
|
68
74
|
}
|
|
69
75
|
export interface ModelHubOptions {
|
|
@@ -129,7 +129,11 @@ export interface SessionSelectorOptions<T extends SessionSelectorEntry = Session
|
|
|
129
129
|
title?: string;
|
|
130
130
|
/** Fixed scope label, or false to omit the scope suffix. */
|
|
131
131
|
scopeLabel?: string | false;
|
|
132
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Show each session's working directory in the list. Defaults to on when
|
|
134
|
+
* the session files live in more than one directory (e.g. the folder scope
|
|
135
|
+
* merging a repository's worktrees).
|
|
136
|
+
*/
|
|
133
137
|
showCwd?: boolean;
|
|
134
138
|
/**
|
|
135
139
|
* Reads the live terminal height so the visible window fits the viewport.
|
|
@@ -33,7 +33,7 @@ export interface ProviderCard {
|
|
|
33
33
|
/** Number of represented accounts, including unavailable usage lookups. */
|
|
34
34
|
accounts: number;
|
|
35
35
|
unavailableAccounts: string[];
|
|
36
|
-
/** Window rows
|
|
36
|
+
/** Window rows in provider-declared order (e.g. 5h → weekly → monthly); the fullest CARD_MAX_WINDOWS lead. */
|
|
37
37
|
windows: CardWindowRow[];
|
|
38
38
|
/** True when every account reports no limits (e.g. enterprise plans). */
|
|
39
39
|
unlimited: boolean;
|
|
@@ -52,8 +52,10 @@ export interface ProviderCard {
|
|
|
52
52
|
* Collapse usage reports into one compact card per provider: limits grouped by
|
|
53
53
|
* quota bucket (label + window), each bucket showing the mean used fraction
|
|
54
54
|
* across accounts (matching the classic report's aggregate "% free") with the
|
|
55
|
-
* most-used account's reset countdown.
|
|
56
|
-
*
|
|
55
|
+
* most-used account's reset countdown. Rows keep the provider's declared window
|
|
56
|
+
* order so 5h → week → month reads the same every time; cards sort by their
|
|
57
|
+
* fullest window so what's burning is on top-left; fully idle providers
|
|
58
|
+
* collapse into a tick.
|
|
57
59
|
*/
|
|
58
60
|
export declare function buildProviderCards(reports: UsageReport[], nowMs: number, unavailableAccounts?: readonly UnavailableUsageAccount[]): ProviderCard[];
|
|
59
61
|
/** GitHub-style week-per-column heatmap grid derived from daily activity. */
|
|
@@ -118,8 +118,8 @@ export declare class CustomEditor extends Editor {
|
|
|
118
118
|
* Numbered by a per-draft monotonic counter so a deleted chip never recycles its number
|
|
119
119
|
* (labels key the atom table). */
|
|
120
120
|
pendingTexts: TextAttachment[];
|
|
121
|
-
/** Host-wired producer of per-image
|
|
122
|
-
* chip tokens for restored drafts (esc-esc, `/tree`, branch). */
|
|
121
|
+
/** Host-wired producer of per-image chip targets (a file on disk, or a session blob copy);
|
|
122
|
+
* drives clickable chip tokens for restored drafts (esc-esc, `/tree`, branch). */
|
|
123
123
|
draftImageLinkMaterializer?: (images: readonly ImageContent[]) => Promise<(string | undefined)[] | undefined>;
|
|
124
124
|
/**
|
|
125
125
|
* The host {@link TUI}, captured when a plugin constructs this editor through
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* to the session's `local://` root, and generated video contact-sheet previews.
|
|
5
5
|
* Symbol metadata stays out of serialized/model-bound image data while traveling
|
|
6
6
|
* with the draft object, until AgentSession creates the hidden companion message
|
|
7
|
-
* that tells the model the path
|
|
7
|
+
* that tells the model the path. The model keeps `local://` references across
|
|
8
|
+
* `/move`; chips for internal URLs instead use a stable blob copy.
|
|
8
9
|
*/
|
|
9
10
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
10
11
|
/** How an image attachment's backing file entered the session. */
|
|
@@ -6,7 +6,7 @@ export { getAvailableThemes, getAvailableThemesWithPaths, getThemeByName, type T
|
|
|
6
6
|
export { isValidThemeColor, type ThemeBg, type ThemeColor } from "./schema.js";
|
|
7
7
|
export { getAvailableSymbolPresets, isValidSymbolPreset, type SpinnerType, type SymbolKey, type SymbolPreset, } from "./symbols.js";
|
|
8
8
|
export { Theme } from "./theme-class.js";
|
|
9
|
-
export { createHighlightStream, getEditorTheme, getMarkdownTheme, getSelectListTheme, getSettingsListTheme, getSymbolTheme, highlightCode, setMarkdownMermaidRendering, warmHighlighter, } from "./tui-adapters.js";
|
|
9
|
+
export { createHighlightStream, getEditorTheme, getMarkdownTheme, getMarkdownThemeWithLinkTargets, getSelectListTheme, getSettingsListTheme, getSymbolTheme, highlightCode, setMarkdownMermaidRendering, warmHighlighter, } from "./tui-adapters.js";
|
|
10
10
|
/** Active theme; imports remain live across initialization, previews, and theme switches. */
|
|
11
11
|
export declare var theme: Theme;
|
|
12
12
|
type ThemeBinding = (value: Theme) => void;
|
|
@@ -16,6 +16,9 @@ export declare function createHighlightStream(lang?: string, highlightTheme?: Th
|
|
|
16
16
|
/** Warm native syntax grammars off-thread once per process. */
|
|
17
17
|
export declare function warmHighlighter(): Promise<void>;
|
|
18
18
|
export declare function getSymbolTheme(): SymbolTheme;
|
|
19
|
+
/** Only themes assembled here have stable callbacks and private link state. */
|
|
20
|
+
export declare function canCacheMarkdownListItems(candidate: MarkdownTheme): boolean;
|
|
21
|
+
export declare function getMarkdownThemeWithLinkTargets(targets: ReadonlyMap<string, string>): MarkdownTheme;
|
|
19
22
|
export declare function setMarkdownMermaidRendering(enabled: boolean): void;
|
|
20
23
|
export declare function getMarkdownTheme(): MarkdownTheme;
|
|
21
24
|
export declare function getSelectListTheme(): SelectListTheme;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.8.
|
|
4
|
+
"version": "18.8.6",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@oh-my-pi/omptype": "18.8.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.8.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.8.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.8.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.8.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.8.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.8.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.8.
|
|
44
|
+
"@oh-my-pi/omptype": "18.8.6",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.8.6",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.8.6",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.8.6",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.8.6",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.8.6",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.8.6",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.8.6"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -105,6 +105,10 @@
|
|
|
105
105
|
"types": "./dist/types/components/*.d.ts",
|
|
106
106
|
"import": "./src/components/*.ts"
|
|
107
107
|
},
|
|
108
|
+
"./native/*": {
|
|
109
|
+
"types": "./dist/types/native/*.d.ts",
|
|
110
|
+
"import": "./src/native/*.ts"
|
|
111
|
+
},
|
|
108
112
|
"./*.js": "./src/*.ts"
|
|
109
113
|
}
|
|
110
114
|
}
|
|
@@ -9,7 +9,7 @@ import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
|
|
|
9
9
|
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
10
10
|
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
11
11
|
import type { AssistantThinkingRenderer } from "./extension-types";
|
|
12
|
-
import { ensureThemeSync, getMarkdownTheme, getThemeEpoch, theme } from "../theme";
|
|
12
|
+
import { ensureThemeSync, getMarkdownTheme, getMarkdownThemeWithLinkTargets, getThemeEpoch, theme } from "../theme";
|
|
13
13
|
import { card, col, elapsed, node, span, text } from "../native/describe";
|
|
14
14
|
import { hasTranscriptActions, runTranscriptAction } from "./transcript-actions";
|
|
15
15
|
import type { NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
@@ -394,7 +394,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
394
394
|
if (this.#markdownTheme) return this.#markdownTheme;
|
|
395
395
|
const base = getMarkdownTheme();
|
|
396
396
|
const snapshot = this.#linkTargets;
|
|
397
|
-
const markdownTheme = snapshot.size > 0 ?
|
|
397
|
+
const markdownTheme = snapshot.size > 0 ? getMarkdownThemeWithLinkTargets(snapshot) : base;
|
|
398
398
|
this.#markdownTheme = markdownTheme;
|
|
399
399
|
return markdownTheme;
|
|
400
400
|
}
|
package/src/chat/user-message.ts
CHANGED
|
@@ -57,7 +57,7 @@ const OSC133_ZONE_CLOSE = OSC133_ZONE_END + OSC133_COMMAND_START + OSC133_COMMAN
|
|
|
57
57
|
|
|
58
58
|
/** How a user bubble styles its prose and chips (see {@link userBubbleColor}). */
|
|
59
59
|
export interface UserBubbleOptions {
|
|
60
|
-
/**
|
|
60
|
+
/** Filesystem paths for attached image chips, indexed by chip number. */
|
|
61
61
|
imageLinks?: readonly (string | undefined)[];
|
|
62
62
|
/** The message's attached images in chip order (`#1` first); a native bubble shows them. */
|
|
63
63
|
images?: readonly ImageContent[];
|
package/src/components/editor.ts
CHANGED
|
@@ -74,9 +74,12 @@ export interface NativeEditorLayout {
|
|
|
74
74
|
readonly caret: string;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Vim mode as the TSP `editor.mode` label. Insert carries none: Insert edits like a plain field,
|
|
79
|
+
* and a host such as Tern stops native editing (selection, ⌘A/⌘C/⌘X) for any labelled mode.
|
|
80
|
+
*/
|
|
81
|
+
const VIM_MODE_LABELS: Record<VimMode, string | undefined> = {
|
|
82
|
+
insert: undefined,
|
|
80
83
|
normal: "NORMAL",
|
|
81
84
|
visual: "VISUAL",
|
|
82
85
|
"visual-line": "VISUAL",
|
|
@@ -25,7 +25,8 @@ import { md } from "../native/describe";
|
|
|
25
25
|
import type { DescribeContext, NativeNode } from "../native/node";
|
|
26
26
|
import type { SymbolTheme } from "../symbols";
|
|
27
27
|
import { TERMINAL } from "../terminal-capabilities";
|
|
28
|
-
import { getSymbolTheme } from "../theme/theme";
|
|
28
|
+
import { getSymbolTheme, getThemeEpoch } from "../theme/theme";
|
|
29
|
+
import { canCacheMarkdownListItems } from "../theme/tui-adapters";
|
|
29
30
|
import type { Component } from "../tui";
|
|
30
31
|
import {
|
|
31
32
|
applyBackgroundToLine,
|
|
@@ -1390,10 +1391,27 @@ interface InlineStyleContext {
|
|
|
1390
1391
|
stylePrefix: string;
|
|
1391
1392
|
}
|
|
1392
1393
|
|
|
1393
|
-
type ListToken = Token & {
|
|
1394
|
+
type ListToken = Token & {
|
|
1395
|
+
items: Array<{ raw: string; tokens?: Token[] }>;
|
|
1396
|
+
ordered: boolean;
|
|
1397
|
+
start?: number;
|
|
1398
|
+
loose: boolean;
|
|
1399
|
+
};
|
|
1394
1400
|
type TableCellToken = { tokens?: Token[] };
|
|
1395
1401
|
type TableToken = Token & { header: TableCellToken[]; rows: TableCellToken[][]; raw?: string };
|
|
1396
1402
|
|
|
1403
|
+
/** `item` as marked lexes it inside a loose list: tight `text` blocks become paragraphs. */
|
|
1404
|
+
function loosenListItem(item: Tokens.ListItem): Tokens.ListItem {
|
|
1405
|
+
if (item.loose) return item;
|
|
1406
|
+
return {
|
|
1407
|
+
...item,
|
|
1408
|
+
loose: true,
|
|
1409
|
+
tokens: item.tokens.map(token =>
|
|
1410
|
+
token.type === "text" ? ({ ...token, type: "paragraph" } as Tokens.Paragraph) : token,
|
|
1411
|
+
),
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1397
1415
|
function formatHyperlink(text: string, target: string): string {
|
|
1398
1416
|
if (!TERMINAL.hyperlinks || !target) {
|
|
1399
1417
|
return text;
|
|
@@ -1743,6 +1761,25 @@ interface TailRowCache extends RenderSignature {
|
|
|
1743
1761
|
// type of token[i+1] when the rows were produced (blank/spacing gate).
|
|
1744
1762
|
nextTypes: (string | undefined)[];
|
|
1745
1763
|
}
|
|
1764
|
+
interface ListItemRowsCache {
|
|
1765
|
+
signature: RenderSignature;
|
|
1766
|
+
width: number;
|
|
1767
|
+
themeEpoch: number;
|
|
1768
|
+
ordered: boolean;
|
|
1769
|
+
start: number | undefined;
|
|
1770
|
+
loose: boolean;
|
|
1771
|
+
raws: string[];
|
|
1772
|
+
rows: RenderedLine[][];
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
interface StreamingListCache {
|
|
1776
|
+
text: string;
|
|
1777
|
+
before: Token[];
|
|
1778
|
+
start: number;
|
|
1779
|
+
tailStart: number;
|
|
1780
|
+
stableCount: number;
|
|
1781
|
+
list: Tokens.List;
|
|
1782
|
+
}
|
|
1746
1783
|
/**
|
|
1747
1784
|
* Mutable per-token record collector passed to #renderContentLines while
|
|
1748
1785
|
* rendering the streaming tail. The render loop fills `raws`/`nextTypes`
|
|
@@ -1856,6 +1893,8 @@ export class Markdown implements Component {
|
|
|
1856
1893
|
// the blank-replacement branch of setText and the fallback branch of
|
|
1857
1894
|
// #lexTokens.
|
|
1858
1895
|
#tailRowCache?: TailRowCache;
|
|
1896
|
+
#listItemRowsCache?: ListItemRowsCache;
|
|
1897
|
+
#streamingListCache?: StreamingListCache;
|
|
1859
1898
|
// True while #renderStreamingContentLines renders the frozen token range:
|
|
1860
1899
|
// frozen code blocks highlight even in transient mode so their bytes match
|
|
1861
1900
|
// the finalized render (they render once into the prefix line cache, so
|
|
@@ -1962,6 +2001,7 @@ export class Markdown implements Component {
|
|
|
1962
2001
|
// the non-append edit, so drop the frozen stream state here or it
|
|
1963
2002
|
// outlives the content it indexed.
|
|
1964
2003
|
this.#dropStreamPrefix();
|
|
2004
|
+
this.#listItemRowsCache = undefined;
|
|
1965
2005
|
// B+: the captured fast-path rows index the replaced content — drop
|
|
1966
2006
|
// the recipe so a fresh stream cannot splice onto stale rows.
|
|
1967
2007
|
this.#fastTail = undefined;
|
|
@@ -2031,7 +2071,9 @@ export class Markdown implements Component {
|
|
|
2031
2071
|
// render consumes it (see #lexTokens / the L2 hit path).
|
|
2032
2072
|
this.#streamPrefixLineCache = undefined;
|
|
2033
2073
|
this.#tailRowCache = undefined;
|
|
2074
|
+
this.#listItemRowsCache = undefined;
|
|
2034
2075
|
this.#streamingHighlightCache = undefined;
|
|
2076
|
+
this.#streamingListCache = undefined;
|
|
2035
2077
|
this.#streamRewound = undefined;
|
|
2036
2078
|
}
|
|
2037
2079
|
this.invalidate();
|
|
@@ -2108,32 +2150,98 @@ export class Markdown implements Component {
|
|
|
2108
2150
|
}
|
|
2109
2151
|
this.#lastScanLength = text.length;
|
|
2110
2152
|
this.#lastScanCanStream = canStream;
|
|
2153
|
+
if (!canStream) this.#listItemRowsCache = undefined;
|
|
2111
2154
|
this.#lastScanValid = true;
|
|
2112
2155
|
this.#appendOnlySinceLastScan = true;
|
|
2156
|
+
if (canStream && retainPrefix) {
|
|
2157
|
+
const tokens = this.#resumeStreamingList(text);
|
|
2158
|
+
if (tokens !== undefined) {
|
|
2159
|
+
this.#freezeStablePrefix(text, tokens, { preserveExisting: true });
|
|
2160
|
+
this.#checkpointStreamingList(text, tokens);
|
|
2161
|
+
return tokens;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2113
2164
|
if (canStream && hasPrefix) {
|
|
2114
|
-
const tailTokens =
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2117
|
-
// can resolve a reference in the frozen prefix, which was lexed
|
|
2165
|
+
const tailTokens = lexDocument(refDefText);
|
|
2166
|
+
// A definition at any nesting depth registers for the whole document
|
|
2167
|
+
// and can resolve a reference in the frozen prefix, which was lexed
|
|
2118
2168
|
// without it, so any definition in the tail sends the text to a full lex.
|
|
2119
2169
|
if (Object.keys(tailTokens.links).length === 0) {
|
|
2120
2170
|
const tokens = [...prefixTokens, ...tailTokens];
|
|
2121
2171
|
if (retainPrefix) this.#freezeStablePrefix(text, tokens, { preserveExisting: true });
|
|
2122
2172
|
else this.#dropStreamPrefix();
|
|
2173
|
+
if (retainPrefix) this.#checkpointStreamingList(text, tokens);
|
|
2123
2174
|
return tokens;
|
|
2124
2175
|
}
|
|
2125
2176
|
}
|
|
2126
|
-
const tokens =
|
|
2177
|
+
const tokens = lexDocument(text);
|
|
2178
|
+
const hasReferenceDefinitions = Object.keys(tokens.links).length > 0;
|
|
2179
|
+
if (hasReferenceDefinitions) this.#lastScanCanStream = false;
|
|
2127
2180
|
// A definition frozen into the prefix would be missing from every later
|
|
2128
2181
|
// tail lex, so a full lex that registered any definition freezes nothing.
|
|
2129
|
-
if (canStream && retainPrefix &&
|
|
2182
|
+
if (canStream && retainPrefix && !hasReferenceDefinitions) {
|
|
2130
2183
|
this.#freezeStablePrefix(text, tokens, { preserveExisting: false });
|
|
2184
|
+
this.#checkpointStreamingList(text, tokens);
|
|
2131
2185
|
} else {
|
|
2132
2186
|
this.#dropStreamPrefix();
|
|
2133
2187
|
}
|
|
2134
2188
|
return tokens;
|
|
2135
2189
|
}
|
|
2136
2190
|
|
|
2191
|
+
#resumeStreamingList(text: string): Token[] | undefined {
|
|
2192
|
+
const cached = this.#streamingListCache;
|
|
2193
|
+
if (cached === undefined || text.length <= cached.text.length || !text.startsWith(cached.text)) return undefined;
|
|
2194
|
+
const tail = lexDocument(text.slice(cached.tailStart));
|
|
2195
|
+
if (Object.keys(tail.links).length > 0) return undefined;
|
|
2196
|
+
const first = tail[0];
|
|
2197
|
+
if (first?.type !== "list") return undefined;
|
|
2198
|
+
const list = first as Tokens.List;
|
|
2199
|
+
if (!listMayContinueAt(list.raw, 0, cached.list.raw)) return undefined;
|
|
2200
|
+
const loose = cached.list.loose || list.loose;
|
|
2201
|
+
const completed = cached.list.items.slice(0, cached.stableCount);
|
|
2202
|
+
const items = [...completed, ...list.items];
|
|
2203
|
+
const merged: Tokens.List = {
|
|
2204
|
+
...cached.list,
|
|
2205
|
+
raw: text.slice(cached.start, cached.tailStart + list.raw.length),
|
|
2206
|
+
loose,
|
|
2207
|
+
items: loose ? items.map(loosenListItem) : items,
|
|
2208
|
+
};
|
|
2209
|
+
return [...cached.before, merged, ...tail.slice(1)];
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
#checkpointStreamingList(text: string, tokens: Token[]): void {
|
|
2213
|
+
const previous = this.#streamingListCache;
|
|
2214
|
+
this.#streamingListCache = undefined;
|
|
2215
|
+
let index = tokens.length - 1;
|
|
2216
|
+
while (tokens[index]?.type === "space") index--;
|
|
2217
|
+
const token = tokens[index];
|
|
2218
|
+
if (token?.type !== "list") return;
|
|
2219
|
+
const list = token as Tokens.List;
|
|
2220
|
+
let start = 0;
|
|
2221
|
+
for (let i = 0; i < index; i++) start += tokens[i]!.raw.length;
|
|
2222
|
+
// Late block delimiters can absorb an unfrozen preceding region. Bare
|
|
2223
|
+
// environments can also absorb list items through an equation prefix.
|
|
2224
|
+
if (start !== (this.#streamPrefixText?.length ?? 0) || BARE_ENV_BEGIN.test(list.raw)) return;
|
|
2225
|
+
const canResume = previous !== undefined && previous.start === start && text.startsWith(previous.text);
|
|
2226
|
+
let cursor = canResume ? previous.tailStart : start;
|
|
2227
|
+
let tailStart = start;
|
|
2228
|
+
let stableCount = 0;
|
|
2229
|
+
const indent = /^ */.exec(list.raw)![0].length;
|
|
2230
|
+
for (let i = canResume ? previous.stableCount : 0; i < list.items.length; i++) {
|
|
2231
|
+
const item = list.items[i]!;
|
|
2232
|
+
const position = text.indexOf(item.raw, cursor);
|
|
2233
|
+
if (position < cursor || position >= start + list.raw.length) return;
|
|
2234
|
+
// List continuation uses the first item's indentation. Resume only at
|
|
2235
|
+
// an item with that same indentation so the lexer sees the same context.
|
|
2236
|
+
if (/^ */.exec(item.raw)![0].length === indent) {
|
|
2237
|
+
tailStart = position;
|
|
2238
|
+
stableCount = i;
|
|
2239
|
+
}
|
|
2240
|
+
cursor = position + item.raw.length;
|
|
2241
|
+
}
|
|
2242
|
+
this.#streamingListCache = { text, before: tokens.slice(0, index), start, tailStart, stableCount, list };
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2137
2245
|
/**
|
|
2138
2246
|
* Drop the frozen lex prefix and the transient row caches keyed on it. A
|
|
2139
2247
|
* prefix set aside for #restoreRewoundPrefix checks itself against each
|
|
@@ -2146,6 +2254,8 @@ export class Markdown implements Component {
|
|
|
2146
2254
|
this.#streamSettledText = undefined;
|
|
2147
2255
|
this.#streamPrefixLineCache = undefined;
|
|
2148
2256
|
this.#tailRowCache = undefined;
|
|
2257
|
+
this.#listItemRowsCache = undefined;
|
|
2258
|
+
this.#streamingListCache = undefined;
|
|
2149
2259
|
}
|
|
2150
2260
|
|
|
2151
2261
|
/**
|
|
@@ -2186,6 +2296,8 @@ export class Markdown implements Component {
|
|
|
2186
2296
|
#rewindStreamPrefix(end: number, count: number): void {
|
|
2187
2297
|
this.#streamPrefixLineCache = rewoundLineCache(this.#streamPrefixLineCache, count);
|
|
2188
2298
|
this.#tailRowCache = undefined;
|
|
2299
|
+
this.#listItemRowsCache = undefined;
|
|
2300
|
+
this.#streamingListCache = undefined;
|
|
2189
2301
|
if (count === 0) {
|
|
2190
2302
|
this.#streamPrefixText = undefined;
|
|
2191
2303
|
this.#streamPrefixTokens = undefined;
|
|
@@ -2223,6 +2335,8 @@ export class Markdown implements Component {
|
|
|
2223
2335
|
this.#streamPrefixOpeners = rewound.openers;
|
|
2224
2336
|
this.#streamPrefixLineCache = rewound.lineCache;
|
|
2225
2337
|
this.#tailRowCache = rewound.tailRowCache;
|
|
2338
|
+
this.#listItemRowsCache = undefined;
|
|
2339
|
+
this.#streamingListCache = undefined;
|
|
2226
2340
|
}
|
|
2227
2341
|
|
|
2228
2342
|
// Freeze the largest run of leading blocks that end on a hard "\n\n" boundary
|
|
@@ -2591,6 +2705,7 @@ export class Markdown implements Component {
|
|
|
2591
2705
|
signature: RenderSignature,
|
|
2592
2706
|
contentWidth: number,
|
|
2593
2707
|
): string[] {
|
|
2708
|
+
if (tokens[tokens.length - 1]?.type !== "list") this.#listItemRowsCache = undefined;
|
|
2594
2709
|
const stableText = this.#streamPrefixText;
|
|
2595
2710
|
const stableTokenCount = this.#streamPrefixTokens?.length ?? 0;
|
|
2596
2711
|
if (stableText === undefined || stableTokenCount === 0 || !normalizedText.startsWith(stableText)) {
|
|
@@ -3594,6 +3709,41 @@ export class Markdown implements Component {
|
|
|
3594
3709
|
const indent = " ".repeat(depth);
|
|
3595
3710
|
// Use the list's start property (defaults to 1 for ordered lists)
|
|
3596
3711
|
const startNumber = token.start ?? 1;
|
|
3712
|
+
const signature =
|
|
3713
|
+
depth === 0 &&
|
|
3714
|
+
styleContext === undefined &&
|
|
3715
|
+
this.#defaultTextStyle === undefined &&
|
|
3716
|
+
this.#transientRenderCache &&
|
|
3717
|
+
!this.#renderingStablePrefix &&
|
|
3718
|
+
this.#lastScanCanStream &&
|
|
3719
|
+
canCacheMarkdownListItems(this.#theme)
|
|
3720
|
+
? this.#activeRenderSignature
|
|
3721
|
+
: undefined;
|
|
3722
|
+
const themeEpoch = signature === undefined ? undefined : getThemeEpoch();
|
|
3723
|
+
const previous = this.#listItemRowsCache;
|
|
3724
|
+
const reusable =
|
|
3725
|
+
signature !== undefined &&
|
|
3726
|
+
themeEpoch !== undefined &&
|
|
3727
|
+
previous !== undefined &&
|
|
3728
|
+
previous.width === width &&
|
|
3729
|
+
previous.themeEpoch === themeEpoch &&
|
|
3730
|
+
previous.ordered === token.ordered &&
|
|
3731
|
+
previous.start === token.start &&
|
|
3732
|
+
previous.loose === token.loose &&
|
|
3733
|
+
this.#signatureEquals(previous.signature, signature)
|
|
3734
|
+
? previous
|
|
3735
|
+
: undefined;
|
|
3736
|
+
let reusableCount = 0;
|
|
3737
|
+
if (reusable !== undefined) {
|
|
3738
|
+
while (
|
|
3739
|
+
reusableCount < token.items.length &&
|
|
3740
|
+
token.items[reusableCount]?.raw === reusable.raws[reusableCount]
|
|
3741
|
+
) {
|
|
3742
|
+
reusableCount++;
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
const raws: string[] | undefined = signature === undefined ? undefined : [];
|
|
3746
|
+
const rows: RenderedLine[][] | undefined = signature === undefined ? undefined : [];
|
|
3597
3747
|
const pushWrapped = (line: RenderedLine, firstPrefix: string, continuationPrefix: string): void => {
|
|
3598
3748
|
if (line.literalCode) {
|
|
3599
3749
|
const wrappedLiteralRows = wrapTextWithAnsi(line.text, Math.max(1, width));
|
|
@@ -3629,6 +3779,14 @@ export class Markdown implements Component {
|
|
|
3629
3779
|
|
|
3630
3780
|
for (let i = 0; i < token.items.length; i++) {
|
|
3631
3781
|
const item = token.items[i];
|
|
3782
|
+
if (i < reusableCount && reusable !== undefined && raws !== undefined && rows !== undefined) {
|
|
3783
|
+
const cached = reusable.rows[i]!;
|
|
3784
|
+
for (const row of cached) lines.push(row);
|
|
3785
|
+
raws.push(item.raw);
|
|
3786
|
+
rows.push(cached);
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
const rowStart = lines.length;
|
|
3632
3790
|
const bullet = token.ordered ? `${startNumber + i}. ` : "- ";
|
|
3633
3791
|
const firstPrefix = indent + this.#theme.listBullet(bullet);
|
|
3634
3792
|
// Continuation rows align under the item text, so the hang matches the
|
|
@@ -3658,6 +3816,22 @@ export class Markdown implements Component {
|
|
|
3658
3816
|
} else {
|
|
3659
3817
|
lines.push(renderedLine(firstPrefix));
|
|
3660
3818
|
}
|
|
3819
|
+
if (raws !== undefined && rows !== undefined) {
|
|
3820
|
+
raws.push(item.raw);
|
|
3821
|
+
rows.push(lines.slice(rowStart));
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
if (signature !== undefined && themeEpoch !== undefined && raws !== undefined && rows !== undefined) {
|
|
3825
|
+
this.#listItemRowsCache = {
|
|
3826
|
+
signature,
|
|
3827
|
+
width,
|
|
3828
|
+
themeEpoch,
|
|
3829
|
+
ordered: token.ordered,
|
|
3830
|
+
start: token.start,
|
|
3831
|
+
loose: token.loose,
|
|
3832
|
+
raws,
|
|
3833
|
+
rows,
|
|
3834
|
+
};
|
|
3661
3835
|
}
|
|
3662
3836
|
|
|
3663
3837
|
return lines;
|
|
@@ -122,6 +122,18 @@ export interface ResolvedModelRoleValue {
|
|
|
122
122
|
warning?: string;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** Where auto-compaction triggers for one model, as the model hub's preview shows it (edited from the Roles view). */
|
|
126
|
+
export interface ModelCompactionPoint {
|
|
127
|
+
/** Context tokens that trigger auto-compaction; undefined when auto-compaction is off or the window is unknown. */
|
|
128
|
+
tokens: number | undefined;
|
|
129
|
+
/** The configured percentage of the window, when the trigger is percent-based. */
|
|
130
|
+
percent?: number;
|
|
131
|
+
/** What sets it: the matching `compaction.modelThresholds` key, `global`, or `default`. */
|
|
132
|
+
source: string;
|
|
133
|
+
/** The model's own exact entry as editable text (`90k`, `80%`); absent when it has none. */
|
|
134
|
+
draft?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
/** Host-provided preferences and model-role resolution for the browser. */
|
|
126
138
|
export interface ModelBrowserSource extends ModelRoleLookup {
|
|
127
139
|
/**
|
|
@@ -143,6 +155,8 @@ export interface ModelBrowserSource extends ModelRoleLookup {
|
|
|
143
155
|
getRoleInfo(role: string): ModelBrowserRoleInfo;
|
|
144
156
|
defaultRoleChain(role: string): string[];
|
|
145
157
|
resolveRoleValue(value: string | undefined, models: Model[], roleLookup?: ModelRoleLookup): ResolvedModelRoleValue;
|
|
158
|
+
/** Auto-compaction point for `model`; absent hosts show no compaction row in the preview. */
|
|
159
|
+
compactionPointFor?(model: Model): ModelCompactionPoint | undefined;
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
/** Read-only catalog surface consumed by model browsers. */
|
|
@@ -2061,7 +2075,7 @@ export class ModelBrowser implements Component {
|
|
|
2061
2075
|
pickerPreview(mode: "full" | "compact", current?: string): readonly NativeChild[] {
|
|
2062
2076
|
const selected = this.getSelected();
|
|
2063
2077
|
const item = selected && !this.#isDisabled(selected) ? selected : undefined;
|
|
2064
|
-
const key = `${mode}\0${current ?? ""}`;
|
|
2078
|
+
const key = `${mode}\0${current ?? ""}\0${this.#settings.revision}`;
|
|
2065
2079
|
const memo = this.#pickerPreview;
|
|
2066
2080
|
if (
|
|
2067
2081
|
memo !== undefined &&
|
|
@@ -2155,6 +2169,17 @@ export class ModelBrowser implements Component {
|
|
|
2155
2169
|
if (v) facts.push({ k: [span(k, "muted")], v: [span(v, "mono")] });
|
|
2156
2170
|
};
|
|
2157
2171
|
fact("Context", ctx > 0 ? ctx.toLocaleString("en-US") : undefined);
|
|
2172
|
+
const compaction = this.#settings.compactionPointFor?.(model);
|
|
2173
|
+
if (compaction) {
|
|
2174
|
+
const value =
|
|
2175
|
+
compaction.tokens === undefined
|
|
2176
|
+
? "off"
|
|
2177
|
+
: `${compaction.tokens.toLocaleString("en-US")}${compaction.percent !== undefined ? ` · ${compaction.percent}%` : ""}`;
|
|
2178
|
+
facts.push({
|
|
2179
|
+
k: [span("Compacts at", "muted")],
|
|
2180
|
+
v: [span(value, "mono"), span(` · ${compaction.source}`, "dim")],
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2158
2183
|
fact("Max output", out > 0 ? out.toLocaleString("en-US") : undefined);
|
|
2159
2184
|
fact("Price", isFreeModel(model) ? "free" : `${previewPrice(model)} per M`);
|
|
2160
2185
|
fact("Speed", speed.length > 0 ? speed.join(" · ") : undefined);
|