@north-light/crouter 0.3.273 → 0.3.275
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/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +11 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +35 -31
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +35 -27
- package/dist/clients/attach/viewer.js +601 -601
- package/dist/core/__tests__/fault-classifier.test.js +47 -0
- package/dist/core/canvas/browse/render.js +82 -67
- package/dist/core/canvas/status-glyph.d.ts +4 -3
- package/dist/core/canvas/status-glyph.js +17 -9
- package/dist/core/fault-classifier.d.ts +0 -15
- package/dist/core/fault-classifier.js +25 -90
- package/dist/core/provider-error-recognition.d.ts +27 -0
- package/dist/core/provider-error-recognition.js +74 -0
- package/dist/core/runtime/surface-bg.d.ts +6 -4
- package/dist/core/runtime/surface-bg.js +39 -22
- package/dist/core/tui/draw.d.ts +2 -0
- package/dist/core/tui/draw.js +15 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasManagedProviderCoolingDiagnostic } from './runtime/managed-provider-cooling.js';
|
|
2
|
+
import { recognizeProviderError } from './provider-error-recognition.js';
|
|
2
3
|
import { isRecord } from '../shared/predicates.js';
|
|
3
4
|
const errorClassByFaultKind = {
|
|
4
5
|
'rate-limit': 'rate_limit',
|
|
@@ -17,85 +18,21 @@ export function errorClassFromFault(fault) {
|
|
|
17
18
|
retry_disposition: fault.retry.disposition,
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// prose ("500 ... request entity too large") and arms an unwinnable retry.
|
|
35
|
-
if (/\b413\b|payload too large|entity too large|request too large|entity\.too\.large|body exceeds/i.test(m))
|
|
36
|
-
return 'protocol';
|
|
37
|
-
if (/invalid_grant|refresh token|unauthori[sz]ed|invalid.?api.?key|authentication failed|\b401\b|\b403\b/i.test(m))
|
|
38
|
-
return 'auth';
|
|
39
|
-
// pi compacts and retries a raw provider overflow. Its failed-recovery result
|
|
40
|
-
// is the point at which the session cannot become viable without a new launch.
|
|
41
|
-
if (/context overflow recovery failed/i.test(m))
|
|
42
|
-
return 'context-overflow';
|
|
43
|
-
if (/websocket.*(?:closed.*\b(?:1002|1003|1007|1008|1009|1010)\b|protocol error|policy violation|unsupported data|invalid (?:frame|payload)|message too big|mandatory extension)/i.test(m))
|
|
44
|
-
return 'protocol';
|
|
45
|
-
// proper-lockfile contention on a credential file: another process holds the
|
|
46
|
-
// mutex right now, so the same request wins once it releases.
|
|
47
|
-
if (/lock file is already being held/i.test(m))
|
|
48
|
-
return 'overloaded';
|
|
49
|
-
if (/overloaded|\b5\d\d\b|internal server error|api.?error|capacity|server.{0,3}busy|temporarily unavailable/i.test(m))
|
|
50
|
-
return 'overloaded';
|
|
51
|
-
if (/connection|econnreset|etimedout|enotfound|econnrefused|network|fetch failed|socket hang|timed? out|timeout|\bterminated\b|other side closed|premature close|websocket\s+(?:was\s+)?closed|websocket stream closed before response\.completed|websocket\s+error|websocket.*interrupt/i.test(m))
|
|
52
|
-
return 'connection';
|
|
53
|
-
return 'other';
|
|
54
|
-
}
|
|
55
|
-
function classifyErrno(raw) {
|
|
56
|
-
const code = text(raw.code) || text(raw.errno);
|
|
57
|
-
if (code === '')
|
|
58
|
-
return null;
|
|
59
|
-
if (/^(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOENT|ENOTFOUND|EAI_AGAIN|EPIPE|EHOSTUNREACH|ENETUNREACH)$/i.test(code)) {
|
|
60
|
-
return 'connection';
|
|
61
|
-
}
|
|
62
|
-
// A held filesystem mutex, not a broken link: back off on time rather than
|
|
63
|
-
// behind the online probe a connection fault waits on.
|
|
64
|
-
if (/^ELOCKED$/i.test(code))
|
|
65
|
-
return 'overloaded';
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
function classifyHttp(raw) {
|
|
69
|
-
const status = typeof raw.status === 'number' ? raw.status : null;
|
|
70
|
-
if (status === null)
|
|
71
|
-
return null;
|
|
72
|
-
if (status === 401 || status === 403)
|
|
73
|
-
return 'auth';
|
|
74
|
-
if (status === 429)
|
|
75
|
-
return 'rate-limit';
|
|
76
|
-
if (status === 503 || status === 529)
|
|
77
|
-
return 'overloaded';
|
|
78
|
-
if (status >= 500 && status <= 599)
|
|
79
|
-
return 'overloaded';
|
|
80
|
-
if (status >= 400 && status <= 499)
|
|
81
|
-
return 'protocol';
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
function classifyWsClose(raw) {
|
|
85
|
-
const code = typeof raw.code === 'number' ? raw.code : null;
|
|
86
|
-
if (code === null)
|
|
87
|
-
return null;
|
|
88
|
-
if (code === 1008)
|
|
89
|
-
return 'protocol';
|
|
90
|
-
if (code === 1006)
|
|
91
|
-
return 'connection';
|
|
92
|
-
if (code === 1011) {
|
|
93
|
-
const reason = text(raw.reason);
|
|
94
|
-
if (/no running broker/i.test(reason))
|
|
95
|
-
return 'connection';
|
|
96
|
-
return classifyProviderText(reason) === 'connection' ? 'connection' : 'other';
|
|
21
|
+
function faultKindFromProviderSignal(signal) {
|
|
22
|
+
switch (signal.kind) {
|
|
23
|
+
case 'rate-limit': return 'rate-limit';
|
|
24
|
+
case 'auth': return 'auth';
|
|
25
|
+
case 'protocol': return 'protocol';
|
|
26
|
+
case 'context-overflow': return 'context-overflow';
|
|
27
|
+
case 'overloaded':
|
|
28
|
+
case 'overloaded-only':
|
|
29
|
+
case 'transient-http': return 'overloaded';
|
|
30
|
+
case 'connection':
|
|
31
|
+
case 'connection-only': return 'connection';
|
|
32
|
+
case 'socket':
|
|
33
|
+
case 'service-unavailable':
|
|
34
|
+
case 'other': return 'other';
|
|
97
35
|
}
|
|
98
|
-
return null;
|
|
99
36
|
}
|
|
100
37
|
function classifyDisposition(link, kind) {
|
|
101
38
|
if (kind === 'auth' || kind === 'protocol' || kind === 'context-overflow')
|
|
@@ -140,17 +77,15 @@ export function classify(link, raw) {
|
|
|
140
77
|
if (link === 'pi→provider' && hasManagedProviderCoolingDiagnostic(raw)) {
|
|
141
78
|
return { kind: 'rate-limit', disposition: 'auto' };
|
|
142
79
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
const kind = classifyProviderText(rawText(raw));
|
|
80
|
+
const value = isRecord(raw) ? raw : {};
|
|
81
|
+
const signal = recognizeProviderError({
|
|
82
|
+
code: value['code'],
|
|
83
|
+
errno: value['errno'],
|
|
84
|
+
status: value['status'],
|
|
85
|
+
wsCloseCode: value['code'],
|
|
86
|
+
wsCloseReason: value['reason'],
|
|
87
|
+
message: rawText(raw),
|
|
88
|
+
});
|
|
89
|
+
const kind = faultKindFromProviderSignal(signal);
|
|
155
90
|
return { kind, disposition: classifyDisposition(link, kind) };
|
|
156
91
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type ProviderErrorSignalKind = 'rate-limit' | 'auth' | 'protocol' | 'context-overflow' | 'overloaded' | 'overloaded-only' | 'transient-http' | 'connection' | 'connection-only' | 'socket' | 'service-unavailable' | 'other';
|
|
2
|
+
export type ProviderErrorMatchedOn = 'errno' | 'status' | 'websocket-close' | 'text' | 'none';
|
|
3
|
+
export interface ProviderErrorTextMatch {
|
|
4
|
+
kind: ProviderErrorSignalKind;
|
|
5
|
+
match: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProviderErrorSignal {
|
|
8
|
+
kind: ProviderErrorSignalKind;
|
|
9
|
+
matchedOn: ProviderErrorMatchedOn;
|
|
10
|
+
status?: number;
|
|
11
|
+
retryAfterMs?: number;
|
|
12
|
+
match?: string;
|
|
13
|
+
textKind?: ProviderErrorSignalKind;
|
|
14
|
+
textMatch?: string;
|
|
15
|
+
textMatches?: readonly ProviderErrorTextMatch[];
|
|
16
|
+
}
|
|
17
|
+
export interface ProviderErrorRecognitionInput {
|
|
18
|
+
status?: unknown;
|
|
19
|
+
retryAfterMs?: number;
|
|
20
|
+
code?: unknown;
|
|
21
|
+
errno?: unknown;
|
|
22
|
+
wsCloseCode?: unknown;
|
|
23
|
+
wsCloseReason?: unknown;
|
|
24
|
+
message?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Recognizes provider failure facts. Consumers apply their own retry policy. */
|
|
27
|
+
export declare function recognizeProviderError(input: ProviderErrorRecognitionInput): ProviderErrorSignal;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function text(value) {
|
|
2
|
+
return typeof value === 'string' ? value : '';
|
|
3
|
+
}
|
|
4
|
+
function signal(kind, matchedOn, options = {}) {
|
|
5
|
+
return { kind, matchedOn, ...options };
|
|
6
|
+
}
|
|
7
|
+
function withTextMatch(signal, textSignal) {
|
|
8
|
+
return textSignal.matchedOn === 'text'
|
|
9
|
+
? { ...signal, textKind: textSignal.kind, textMatch: textSignal.match, textMatches: textSignal.textMatches }
|
|
10
|
+
: signal;
|
|
11
|
+
}
|
|
12
|
+
const TEXT_PATTERNS = [
|
|
13
|
+
['rate-limit', /rate.?limit|\b429\b|too many requests|usage[ _]limit|usage_not_included|usagelimiterror|insufficient_quota|quota exceeded/i],
|
|
14
|
+
['protocol', /\b413\b|payload too large|entity too large|request too large|entity\.too\.large|body exceeds/i],
|
|
15
|
+
['auth', /invalid_grant|refresh token|unauthori[sz]ed|invalid.?api.?key|authentication failed|\b401\b|\b403\b/i],
|
|
16
|
+
['context-overflow', /context overflow recovery failed/i],
|
|
17
|
+
['protocol', /websocket.*(?:closed.*\b(?:1002|1003|1007|1008|1009|1010)\b|protocol error|policy violation|unsupported data|invalid (?:frame|payload)|message too big|mandatory extension)/i],
|
|
18
|
+
['overloaded-only', /lock file is already being held/i],
|
|
19
|
+
['overloaded', /overloaded|temporarily unavailable/i],
|
|
20
|
+
['service-unavailable', /service.?unavailable/i],
|
|
21
|
+
['overloaded-only', /internal server error|api.?error|\b5\d\d\b|capacity|server.{0,3}busy/i],
|
|
22
|
+
['connection', /connection|econnreset|etimedout|enotfound|econnrefused|network|fetch failed|socket hang|timed? out|timeout|websocket\s+(?:was\s+)?closed|websocket stream closed before response\.completed|websocket\s+error|websocket.*interrupt/i],
|
|
23
|
+
['socket', /socket/i],
|
|
24
|
+
['connection-only', /eai_again|epipe|ehostunreach|enetunreach|\bterminated\b|other side closed|premature close/i],
|
|
25
|
+
];
|
|
26
|
+
function recognizeText(message) {
|
|
27
|
+
// The order preserves the canonical taxonomy: size wins over wrapped 5xx prose,
|
|
28
|
+
// and auth wins over a generic API-error wrapper. Every match remains available
|
|
29
|
+
// because rotation deliberately gives transient text priority over rate-limit text.
|
|
30
|
+
const matches = TEXT_PATTERNS.flatMap(([kind, pattern]) => {
|
|
31
|
+
const match = message.match(pattern);
|
|
32
|
+
return match ? [{ kind, match: match[0] }] : [];
|
|
33
|
+
});
|
|
34
|
+
const first = matches[0];
|
|
35
|
+
return first === undefined ? signal('other', 'none') : { ...signal(first.kind, 'text', { match: first.match }), textMatches: matches };
|
|
36
|
+
}
|
|
37
|
+
/** Recognizes provider failure facts. Consumers apply their own retry policy. */
|
|
38
|
+
export function recognizeProviderError(input) {
|
|
39
|
+
const retryAfterMs = input.retryAfterMs;
|
|
40
|
+
const textSignal = recognizeText(input.message ?? '');
|
|
41
|
+
const errno = text(input.code) || text(input.errno);
|
|
42
|
+
if (/^(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOENT|ENOTFOUND|EAI_AGAIN|EPIPE|EHOSTUNREACH|ENETUNREACH)$/i.test(errno)) {
|
|
43
|
+
return withTextMatch(signal('connection', 'errno', { retryAfterMs }), textSignal);
|
|
44
|
+
}
|
|
45
|
+
if (/^ELOCKED$/i.test(errno))
|
|
46
|
+
return withTextMatch(signal('overloaded', 'errno', { retryAfterMs }), textSignal);
|
|
47
|
+
const status = typeof input.status === 'number' ? input.status : undefined;
|
|
48
|
+
if (status !== undefined) {
|
|
49
|
+
if (status === 401 || status === 403)
|
|
50
|
+
return withTextMatch(signal('auth', 'status', { status, retryAfterMs }), textSignal);
|
|
51
|
+
if (status === 429)
|
|
52
|
+
return withTextMatch(signal('rate-limit', 'status', { status, retryAfterMs }), textSignal);
|
|
53
|
+
if (status === 500 || status === 502 || status === 503 || status === 504)
|
|
54
|
+
return withTextMatch(signal('transient-http', 'status', { status, retryAfterMs }), textSignal);
|
|
55
|
+
if (status >= 500 && status <= 599)
|
|
56
|
+
return withTextMatch(signal('overloaded', 'status', { status, retryAfterMs }), textSignal);
|
|
57
|
+
if (status >= 400 && status <= 499)
|
|
58
|
+
return withTextMatch(signal('protocol', 'status', { status, retryAfterMs }), textSignal);
|
|
59
|
+
}
|
|
60
|
+
const wsCloseCode = typeof input.wsCloseCode === 'number' ? input.wsCloseCode : undefined;
|
|
61
|
+
if (wsCloseCode === 1008)
|
|
62
|
+
return withTextMatch(signal('protocol', 'websocket-close', { retryAfterMs }), textSignal);
|
|
63
|
+
if (wsCloseCode === 1006)
|
|
64
|
+
return withTextMatch(signal('connection', 'websocket-close', { retryAfterMs }), textSignal);
|
|
65
|
+
if (wsCloseCode === 1011) {
|
|
66
|
+
const reason = text(input.wsCloseReason);
|
|
67
|
+
const reasonSignal = recognizeText(reason);
|
|
68
|
+
if (/no running broker/i.test(reason) || reasonSignal.kind === 'connection' || reasonSignal.kind === 'connection-only') {
|
|
69
|
+
return withTextMatch(signal('connection', 'websocket-close', { retryAfterMs }), textSignal);
|
|
70
|
+
}
|
|
71
|
+
return withTextMatch(signal('other', 'websocket-close', { retryAfterMs }), textSignal);
|
|
72
|
+
}
|
|
73
|
+
return retryAfterMs === undefined ? textSignal : { ...textSignal, retryAfterMs };
|
|
74
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
/** The SGR escape that turns ON the distinct-surface background (theme
|
|
2
|
-
*
|
|
1
|
+
/** The SGR escape that turns ON the distinct-surface background (the theme's
|
|
2
|
+
* card plane), e.g. `\x1b[48;2;52;53;65m`. Pair with `\x1b[49m` to reset just
|
|
3
3
|
* the background. Throws if no process has initialized the theme — callers on a
|
|
4
4
|
* render path (the attach overlay) always have, by construction. */
|
|
5
5
|
export declare function surfaceBgAnsi(): string;
|
|
6
|
-
/** tmux flags that frame a `display-menu` / `display-popup`
|
|
7
|
-
* surface
|
|
6
|
+
/** tmux flags that frame a `display-menu` / `display-popup` as a distinct
|
|
7
|
+
* surface — a rounded border in the theme accent around a body painted with the
|
|
8
|
+
* theme's card plane. The accent border is what carries the "you are somewhere
|
|
9
|
+
* else" signal, because a card sits close to the page by design.
|
|
8
10
|
* Empty when the theme is unavailable in this process (chrome stays default; the
|
|
9
11
|
* attach viewer re-installs the menu with these once it has themed). One knob:
|
|
10
12
|
* every crtr tmux float shares this single definition. */
|
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
// crtr's overlays and popups must read as a SEPARATE surface from the normal one
|
|
4
4
|
// (CTO ruling, insights/terminal-ui/inline-ui-placement #11): a popup whose background matches
|
|
5
5
|
// the surface has invisible edges, so you can't tell where the float begins. We
|
|
6
|
-
// paint them
|
|
7
|
-
//
|
|
6
|
+
// paint them from the active pi theme — theme-derived, so it adapts to light/dark
|
|
7
|
+
// and is ONE centralized knob, not a scattered hardcode.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// WHICH ROLE: `userMessageBg`, the theme's CARD plane — not `selectedBg`. Every
|
|
10
|
+
// theme places its card one step off the page background (gruvbox-material
|
|
11
|
+
// #32302f vs #282828; pi's own dark #343541, light #e8e8e8) while `selectedBg` is
|
|
12
|
+
// a SELECTION BAND, deliberately pushed far off the page so a highlighted row
|
|
13
|
+
// lifts. Painting a 90%×85% takeover with the selection band made the whole
|
|
14
|
+
// screen that band: every dim/muted role lost the contrast it was designed
|
|
15
|
+
// against (gruvbox dim text fell to 2.1:1), and the row highlight drawn ON TOP
|
|
16
|
+
// had nowhere left to go. The card plane keeps content legibility and leaves the
|
|
17
|
+
// selection band free to do its actual job inside the popup.
|
|
18
|
+
//
|
|
19
|
+
// A card sits close to the page by design, so the EDGE signal rule 11 demands
|
|
20
|
+
// comes from the border, not the fill: we paint the rounded border in the theme
|
|
21
|
+
// accent so the float's boundary is unmistakable.
|
|
22
|
+
//
|
|
23
|
+
// pi 0.79 does not re-export these roles through its public API (the `.` entry
|
|
10
24
|
// omits `getResolvedThemeColors` and the live `theme` singleton). It DOES publish
|
|
11
25
|
// the live `Theme` instance on a process-global symbol once `initTheme()` has run
|
|
12
26
|
// — the same hook pi's own SDK reads (see broker.ts `ctx.ui.theme`). We read it
|
|
@@ -26,43 +40,46 @@ function liveTheme() {
|
|
|
26
40
|
const t = globalThis[THEME_SYMBOL];
|
|
27
41
|
return (t ?? undefined);
|
|
28
42
|
}
|
|
29
|
-
/** The SGR escape that turns ON the distinct-surface background (theme
|
|
30
|
-
*
|
|
43
|
+
/** The SGR escape that turns ON the distinct-surface background (the theme's
|
|
44
|
+
* card plane), e.g. `\x1b[48;2;52;53;65m`. Pair with `\x1b[49m` to reset just
|
|
31
45
|
* the background. Throws if no process has initialized the theme — callers on a
|
|
32
46
|
* render path (the attach overlay) always have, by construction. */
|
|
33
47
|
export function surfaceBgAnsi() {
|
|
34
48
|
const t = liveTheme();
|
|
35
49
|
if (t === undefined)
|
|
36
50
|
throw new Error('surfaceBgAnsi: pi theme not initialized in this process');
|
|
37
|
-
return t.getBgAnsi('
|
|
51
|
+
return t.getBgAnsi('userMessageBg');
|
|
38
52
|
}
|
|
39
|
-
/**
|
|
40
|
-
* truecolor theme, `colourN` on a 256-colour
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
function
|
|
44
|
-
const
|
|
45
|
-
if (t === undefined)
|
|
46
|
-
return undefined;
|
|
47
|
-
const ansi = t.getBgAnsi('selectedBg');
|
|
48
|
-
const tc = /48;2;(\d+);(\d+);(\d+)/.exec(ansi);
|
|
53
|
+
/** An SGR colour parameter (`48;2;r;g;b` or `38;5;n`) as a tmux colour token —
|
|
54
|
+
* `#rrggbb` on a truecolor theme, `colourN` on a 256-colour one. tmux cannot
|
|
55
|
+
* read pi's theme, so we translate the SGR pi already resolved for us.
|
|
56
|
+
* undefined when the role is empty (the SGR is a bare default-colour reset). */
|
|
57
|
+
function tmuxColor(ansi) {
|
|
58
|
+
const tc = /[34]8;2;(\d+);(\d+);(\d+)/.exec(ansi);
|
|
49
59
|
if (tc !== null) {
|
|
50
60
|
const hex = (n) => Number(n).toString(16).padStart(2, '0');
|
|
51
61
|
return `#${hex(tc[1])}${hex(tc[2])}${hex(tc[3])}`;
|
|
52
62
|
}
|
|
53
|
-
const c256 = /
|
|
63
|
+
const c256 = /[34]8;5;(\d+)/.exec(ansi);
|
|
54
64
|
if (c256 !== null)
|
|
55
65
|
return `colour${c256[1]}`;
|
|
56
|
-
return undefined;
|
|
66
|
+
return undefined;
|
|
57
67
|
}
|
|
58
|
-
/** tmux flags that frame a `display-menu` / `display-popup`
|
|
59
|
-
* surface
|
|
68
|
+
/** tmux flags that frame a `display-menu` / `display-popup` as a distinct
|
|
69
|
+
* surface — a rounded border in the theme accent around a body painted with the
|
|
70
|
+
* theme's card plane. The accent border is what carries the "you are somewhere
|
|
71
|
+
* else" signal, because a card sits close to the page by design.
|
|
60
72
|
* Empty when the theme is unavailable in this process (chrome stays default; the
|
|
61
73
|
* attach viewer re-installs the menu with these once it has themed). One knob:
|
|
62
74
|
* every crtr tmux float shares this single definition. */
|
|
63
75
|
export function surfaceTmuxStyleArgs() {
|
|
64
|
-
const
|
|
76
|
+
const t = liveTheme();
|
|
77
|
+
if (t === undefined)
|
|
78
|
+
return [];
|
|
79
|
+
const bg = tmuxColor(t.getBgAnsi('userMessageBg'));
|
|
65
80
|
if (bg === undefined)
|
|
66
81
|
return [];
|
|
67
|
-
|
|
82
|
+
const accent = tmuxColor(t.getFgAnsi('accent'));
|
|
83
|
+
const border = accent === undefined ? `bg=${bg}` : `fg=${accent},bg=${bg}`;
|
|
84
|
+
return ['-b', 'rounded', '-s', `bg=${bg}`, '-S', border];
|
|
68
85
|
}
|
package/dist/core/tui/draw.d.ts
CHANGED
package/dist/core/tui/draw.js
CHANGED
|
@@ -22,6 +22,21 @@ const REVERSE = `${ESC}7m`;
|
|
|
22
22
|
const DIM = `${ESC}2m`;
|
|
23
23
|
const BOLD = `${ESC}1m`;
|
|
24
24
|
const ITALIC = `${ESC}3m`;
|
|
25
|
+
// ── The recessive text ramp ────────────────────────────────────────────
|
|
26
|
+
//
|
|
27
|
+
// Two explicit greys for text that must sit BEHIND the primary content without
|
|
28
|
+
// disappearing into it. Use these instead of the two things that look equivalent
|
|
29
|
+
// and are not:
|
|
30
|
+
//
|
|
31
|
+
// - basic-16 bright-black (`90`) resolves against the TERMINAL's palette, which
|
|
32
|
+
// a dark theme routinely sets within a hair of its own background;
|
|
33
|
+
// - SGR DIM (`2`) is a multiplier, so stacking it on an already-dark grey lands
|
|
34
|
+
// at mud — and stacking it on THESE would undo the contrast they encode.
|
|
35
|
+
//
|
|
36
|
+
// Never combine either with these. Ratios below are against the two planes a
|
|
37
|
+
// raw-ANSI view can render on: a bare dark terminal and a themed popup card.
|
|
38
|
+
export const META_FG = '38;5;246'; // 4.3–4.9:1 — metadata you actively scan
|
|
39
|
+
export const RECESSIVE_FG = '38;5;243'; // 2.9–3.3:1 — deliberately behind, still read
|
|
25
40
|
/** An SGR fg/bg parameter is digits and semicolons only (e.g. '32', '1;36', '236').
|
|
26
41
|
* Guards styleSpan against a non-numeric value (a color name) producing a broken
|
|
27
42
|
* escape sequence. */
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.275",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.275",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|