@oh-my-pi/pi-tui 18.4.8 → 18.4.10
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 +41 -0
- package/dist/types/apps/debug/log-viewer.d.ts +17 -0
- package/dist/types/apps/debug/raw-sse.d.ts +14 -0
- package/dist/types/chat/assistant-message.d.ts +4 -3
- package/dist/types/chat/messages.d.ts +4 -0
- package/dist/types/chrome/transcript-container.d.ts +7 -0
- package/dist/types/components/markdown.d.ts +0 -4
- package/dist/types/native/backend.d.ts +8 -0
- package/dist/types/native/encode.d.ts +5 -1
- package/dist/types/native/node.d.ts +14 -1
- package/dist/types/native/reconcile.d.ts +5 -0
- package/dist/types/overlays/advisor-config.d.ts +8 -0
- package/dist/types/overlays/ask-dialog.d.ts +1 -0
- package/dist/types/overlays/extensions/extension-dashboard.d.ts +31 -0
- package/dist/types/overlays/extensions/extension-list.d.ts +37 -0
- package/dist/types/overlays/extensions/inspector-panel.d.ts +8 -0
- package/dist/types/overlays/jobs-panel.d.ts +35 -9
- package/dist/types/prompt/composer.d.ts +3 -1
- package/dist/types/prompt/word-completion.d.ts +12 -0
- package/dist/types/status-line/context-usage.d.ts +2 -0
- package/dist/types/terminal-capabilities.d.ts +5 -0
- package/dist/types/terminal.d.ts +7 -0
- package/dist/types/tools/bash-interactive.d.ts +15 -0
- package/dist/types/tools/output-meta.d.ts +8 -2
- package/dist/types/tools/streaming-output.d.ts +15 -10
- package/dist/types/tui.d.ts +8 -0
- package/dist/types/windows-input-mode.d.ts +26 -0
- package/package.json +9 -9
- package/src/apps/debug/log-viewer.ts +236 -2
- package/src/apps/debug/protocol-probe.ts +10 -1
- package/src/apps/debug/raw-sse.ts +210 -28
- package/src/chat/advisor-message.ts +4 -2
- package/src/chat/assistant-message.ts +7 -9
- package/src/chat/chat-transcript-builder.ts +1 -1
- package/src/chat/messages.ts +4 -0
- package/src/chrome/transcript-container.ts +13 -0
- package/src/components/markdown.ts +90 -130
- package/src/keys.ts +2 -4
- package/src/native/apply.ts +2 -1
- package/src/native/backend.ts +22 -0
- package/src/native/encode.ts +9 -2
- package/src/native/node.ts +15 -1
- package/src/native/reconcile.ts +28 -1
- package/src/overlays/advisor-config.ts +228 -2
- package/src/overlays/ask-dialog.ts +1 -0
- package/src/overlays/extensions/extension-dashboard.ts +275 -23
- package/src/overlays/extensions/extension-list.ts +274 -14
- package/src/overlays/extensions/inspector-panel.ts +368 -0
- package/src/overlays/hook-selector.ts +1 -1
- package/src/overlays/jobs-panel.ts +222 -49
- package/src/overlays/model-hub.ts +25 -4
- package/src/overlays/session-info-overlay.ts +55 -11
- package/src/prompt/composer-cache.ts +26 -3
- package/src/prompt/composer.ts +32 -7
- package/src/prompt/word-completion.ts +31 -13
- package/src/status-line/context-usage.ts +18 -2
- package/src/terminal-capabilities.ts +8 -0
- package/src/terminal.ts +43 -7
- package/src/tools/bash-interactive.ts +85 -16
- package/src/tools/output-meta.ts +13 -5
- package/src/tools/streaming-output.ts +48 -23
- package/src/tools/wait.ts +7 -19
- package/src/tui.ts +56 -14
- package/src/windows-input-mode.ts +206 -0
|
@@ -6,12 +6,11 @@ export declare const DEFAULT_MAX_BYTES: number;
|
|
|
6
6
|
/** Default UTF-8 byte budget per match line. */
|
|
7
7
|
export declare const DEFAULT_MAX_COLUMN = 512;
|
|
8
8
|
/**
|
|
9
|
-
* Default artifact-on-disk cap for {@link OutputSink}
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* complete raw stream instead of a capped head/tail sample.
|
|
9
|
+
* Default artifact-on-disk cap for {@link OutputSink}: 16 MiB, split into a
|
|
10
|
+
* {@link ARTIFACT_DEFAULT_HEAD_BYTES} head and a rolling tail. `0` means
|
|
11
|
+
* unbounded (the complete raw stream is preserved).
|
|
13
12
|
*/
|
|
14
|
-
export declare const ARTIFACT_DEFAULT_MAX_BYTES
|
|
13
|
+
export declare const ARTIFACT_DEFAULT_MAX_BYTES: number;
|
|
15
14
|
/** Default head budget; the remainder becomes the rolling tail window. */
|
|
16
15
|
export declare const ARTIFACT_DEFAULT_HEAD_BYTES: number;
|
|
17
16
|
/** First failed artifact I/O operation; safe to persist without exposing filesystem errors. */
|
|
@@ -36,6 +35,11 @@ export interface OutputSummary {
|
|
|
36
35
|
columnMax?: number;
|
|
37
36
|
/** Artifact ID for internal URL access (artifact://<id>) when truncated */
|
|
38
37
|
artifactId?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Bytes the artifact cap dropped from the middle of the saved file. When
|
|
40
|
+
* set, `artifact://<id>` holds a head/tail sample, not the full output.
|
|
41
|
+
*/
|
|
42
|
+
artifactElidedBytes?: number;
|
|
39
43
|
/** Full raw output was not completely saved; artifactId is unavailable. */
|
|
40
44
|
artifactError?: OutputArtifactError;
|
|
41
45
|
}
|
|
@@ -67,17 +71,18 @@ export interface OutputSinkOptions {
|
|
|
67
71
|
/** Minimum ms between onChunk calls. 0 = every chunk (default). */
|
|
68
72
|
chunkThrottleMs?: number;
|
|
69
73
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
74
|
+
* Cap on bytes written to the artifact-on-disk file. When the cap is hit,
|
|
75
|
+
* the head window is preserved verbatim and subsequent output feeds a
|
|
76
|
+
* rolling tail window; on close, the sink writes a single
|
|
73
77
|
* `[ARTIFACT TRUNCATED: …]` notice between them. Default
|
|
74
|
-
* {@link ARTIFACT_DEFAULT_MAX_BYTES} (unbounded
|
|
78
|
+
* {@link ARTIFACT_DEFAULT_MAX_BYTES} (16 MiB); `0` = unbounded.
|
|
75
79
|
*/
|
|
76
80
|
artifactMaxBytes?: number;
|
|
77
81
|
/**
|
|
78
82
|
* Bytes reserved for the head window of the capped artifact file. The
|
|
79
83
|
* tail window receives `artifactMaxBytes - artifactHeadBytes`. Default
|
|
80
|
-
* {@link ARTIFACT_DEFAULT_HEAD_BYTES}
|
|
84
|
+
* {@link ARTIFACT_DEFAULT_HEAD_BYTES}, but at most half of
|
|
85
|
+
* `artifactMaxBytes`; clamped to `[0, artifactMaxBytes]`.
|
|
81
86
|
*/
|
|
82
87
|
artifactHeadBytes?: number;
|
|
83
88
|
}
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -111,6 +111,9 @@ export interface Component {
|
|
|
111
111
|
* last time.
|
|
112
112
|
*/
|
|
113
113
|
render(width: number): readonly string[];
|
|
114
|
+
/** Inline decision panels must keep displaced settled transcript rows in
|
|
115
|
+
* native scrollback even when mounted inside a transient editor container. */
|
|
116
|
+
readonly retireDisplacedTranscript?: boolean;
|
|
114
117
|
/**
|
|
115
118
|
* Describe the component semantically for a Tern Surface Protocol
|
|
116
119
|
* terminal (see `native/node.ts`). Called instead of `render()` when the
|
|
@@ -330,6 +333,11 @@ export declare class TUI extends Container {
|
|
|
330
333
|
options?: OverlayOptions;
|
|
331
334
|
preFocus: Component | null;
|
|
332
335
|
hidden: boolean;
|
|
336
|
+
/**
|
|
337
|
+
* A sheet the user clicked away from (see {@link TUI.#focusFromPointer}):
|
|
338
|
+
* it stays up but no longer holds the keys, until focus moves back into it.
|
|
339
|
+
*/
|
|
340
|
+
released: boolean;
|
|
333
341
|
}[];
|
|
334
342
|
constructor(terminal: Terminal, showHardwareCursor?: boolean, options?: TUIOptions);
|
|
335
343
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode Windows console win32-input-mode key records into the key sequences
|
|
3
|
+
* the rest of the TUI already understands.
|
|
4
|
+
*
|
|
5
|
+
* Hosts without the kitty keyboard protocol (Windows Terminal before 1.25)
|
|
6
|
+
* deliver Shift+Enter as a bare `\r` (indistinguishable from Enter) and
|
|
7
|
+
* Ctrl+Enter as `\n`. Enabling `CSI ? 9001 h` makes the console host serving
|
|
8
|
+
* this process report every key as `CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _`, which
|
|
9
|
+
* carries the full modifier state. Paste text, mouse reports and terminal
|
|
10
|
+
* replies keep arriving as plain VT, so only key records need translating.
|
|
11
|
+
*
|
|
12
|
+
* Unmodified and legacy-expressible keys translate to the bytes a legacy
|
|
13
|
+
* terminal sends; chords legacy encoding loses (modified Enter/Tab/Escape,
|
|
14
|
+
* Ctrl+Shift+letter, …) translate to kitty CSI-u, which key matching already
|
|
15
|
+
* parses.
|
|
16
|
+
*/
|
|
17
|
+
/** Stateful decoder: joins UTF-16 surrogate halves that arrive as separate records. */
|
|
18
|
+
export declare class Win32InputModeDecoder {
|
|
19
|
+
#private;
|
|
20
|
+
/**
|
|
21
|
+
* Translate one stdin sequence. Returns `undefined` when `data` is not a
|
|
22
|
+
* win32-input-mode record (pass it through unchanged), otherwise the key
|
|
23
|
+
* events it produces — empty for key releases and bare modifier presses.
|
|
24
|
+
*/
|
|
25
|
+
decode(data: string): string[] | undefined;
|
|
26
|
+
}
|
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.4.
|
|
4
|
+
"version": "18.4.10",
|
|
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.4.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.4.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.4.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.4.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.4.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.4.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.4.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.4.
|
|
44
|
+
"@oh-my-pi/omptype": "18.4.10",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.4.10",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.4.10",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.4.10",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.4.10",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.4.10",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.4.10",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.4.10"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import type { TspPickerGroup, TspPickerItem, TspTone } from "@oh-my-pi/pi-wire";
|
|
1
2
|
import { formatKeyHint, formatKeyHints } from "../../app-keybindings";
|
|
2
3
|
import type { Component } from "../../tui";
|
|
4
|
+
import { code } from "../../native/describe";
|
|
5
|
+
import { Memo } from "../../native/memo";
|
|
6
|
+
import type { DescribeContext, NativeNode, NativeUiEvent } from "../../native/node";
|
|
7
|
+
import { CLOSE_ACTION, picker, pickerAction, pickerEvent, pickerHits, pickerQuery } from "../../native/picker";
|
|
3
8
|
import { matchesKey } from "../../keys";
|
|
4
9
|
import { Input } from "../../components/input";
|
|
5
10
|
import { routeSgrMouseInput, type SgrMouseEvent } from "../../mouse";
|
|
6
11
|
import { padding, truncateToWidth, visibleWidth } from "../../utils";
|
|
7
|
-
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import { isRecord, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
8
13
|
import { theme } from "../../theme/theme";
|
|
9
14
|
import { sanitizeDisplayText } from "../../overlays/extensions/display-text";
|
|
10
15
|
import { DebugViewerFrame, type DebugViewerFrameContent, type DebugViewerFrameContext } from "./viewer-frame";
|
|
@@ -24,6 +29,15 @@ export const LOAD_OLDER_LABEL = "### MOVE UP TO LOAD MORE...";
|
|
|
24
29
|
const INITIAL_LOG_CHUNK = 50;
|
|
25
30
|
const LOAD_OLDER_CHUNK = 50;
|
|
26
31
|
const MIN_LOG_VIEWER_WIDTH = 48;
|
|
32
|
+
/** Picker item id of the "load older entries" row. */
|
|
33
|
+
const OLDER_ITEM = "older";
|
|
34
|
+
/** Status-dot tone of a log level. */
|
|
35
|
+
const LEVEL_TONES: Readonly<Record<string, TspTone>> = {
|
|
36
|
+
error: "error",
|
|
37
|
+
warn: "warning",
|
|
38
|
+
info: "info",
|
|
39
|
+
debug: "muted",
|
|
40
|
+
};
|
|
27
41
|
|
|
28
42
|
type LogEntry = {
|
|
29
43
|
rawLine: string;
|
|
@@ -40,6 +54,47 @@ type DebugLogViewerModelOptions = {
|
|
|
40
54
|
loadOlderLogs?: (limitDays?: number) => Promise<string>;
|
|
41
55
|
};
|
|
42
56
|
|
|
57
|
+
/**
|
|
58
|
+
* One log entry as a picker row: the message as the label, the remaining
|
|
59
|
+
* fields as `key=value` detail, the local time and pid as facts and the
|
|
60
|
+
* level as the status dot. Lines that aren't JSON objects show as they are.
|
|
61
|
+
*/
|
|
62
|
+
function logPickerItem(id: string, rawLine: string): TspPickerItem {
|
|
63
|
+
let parsed: unknown;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(rawLine);
|
|
66
|
+
} catch {}
|
|
67
|
+
if (!isRecord(parsed)) {
|
|
68
|
+
return { id, label: sanitizeDisplayText(rawLine).replace(/\s+/g, " "), mono: true };
|
|
69
|
+
}
|
|
70
|
+
const { timestamp, level, pid, message, ...rest } = parsed;
|
|
71
|
+
let detail = "";
|
|
72
|
+
for (const key in rest) {
|
|
73
|
+
const value = rest[key];
|
|
74
|
+
detail += `${detail ? " " : ""}${key}=${typeof value === "string" ? value : JSON.stringify(value)}`;
|
|
75
|
+
}
|
|
76
|
+
const time = typeof timestamp === "string" ? new Date(timestamp) : undefined;
|
|
77
|
+
return {
|
|
78
|
+
id,
|
|
79
|
+
label: sanitizeDisplayText(typeof message === "string" ? message : rawLine).replace(/\s+/g, " "),
|
|
80
|
+
detail: detail ? sanitizeDisplayText(detail).replace(/\s+/g, " ") : undefined,
|
|
81
|
+
dot: typeof level === "string" ? LEVEL_TONES[level] : undefined,
|
|
82
|
+
facts: {
|
|
83
|
+
time: time && Number.isFinite(time.getTime()) ? time.toLocaleTimeString(undefined, { hour12: false }) : "",
|
|
84
|
+
pid: typeof pid === "number" ? String(pid) : "",
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A log line pretty-printed for the preview pane: indented JSON when it parses, else the sanitized line. */
|
|
90
|
+
function logPreviewText(rawLine: string): { text: string; lang?: string } {
|
|
91
|
+
try {
|
|
92
|
+
return { text: JSON.stringify(JSON.parse(rawLine), null, 2), lang: "json" };
|
|
93
|
+
} catch {
|
|
94
|
+
return { text: sanitizeDisplayText(rawLine) };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
43
98
|
type ViewerRow =
|
|
44
99
|
| {
|
|
45
100
|
kind: "warning";
|
|
@@ -516,6 +571,10 @@ export class DebugLogViewerComponent implements Component {
|
|
|
516
571
|
#statusMessage: string | undefined;
|
|
517
572
|
#loadingOlder = false;
|
|
518
573
|
#bodyLineToRowIndex: Array<number | undefined> = [];
|
|
574
|
+
readonly #native = new Memo();
|
|
575
|
+
/** Picker rows by log index, valid while {@link #nativeItemsCount} entries are loaded (a prepend shifts indices). */
|
|
576
|
+
#nativeItems = new Map<number, TspPickerItem>();
|
|
577
|
+
#nativeItemsCount = 0;
|
|
519
578
|
|
|
520
579
|
constructor(options: DebugLogViewerComponentOptions) {
|
|
521
580
|
this.#deps = options.deps;
|
|
@@ -606,6 +665,24 @@ export class DebugLogViewerComponent implements Component {
|
|
|
606
665
|
return;
|
|
607
666
|
}
|
|
608
667
|
|
|
668
|
+
// Page and end keys jump the cursor; the frame (or the native list)
|
|
669
|
+
// keeps it in view.
|
|
670
|
+
const jump = matchesKey(keyData, "pageUp")
|
|
671
|
+
? -this.#frame.getBodyHeight()
|
|
672
|
+
: matchesKey(keyData, "pageDown")
|
|
673
|
+
? this.#frame.getBodyHeight()
|
|
674
|
+
: matchesKey(keyData, "home")
|
|
675
|
+
? -this.#model.rows.length
|
|
676
|
+
: matchesKey(keyData, "end")
|
|
677
|
+
? this.#model.rows.length
|
|
678
|
+
: 0;
|
|
679
|
+
if (jump !== 0) {
|
|
680
|
+
this.#statusMessage = undefined;
|
|
681
|
+
this.#model.moveCursor(jump, false);
|
|
682
|
+
this.#onUpdate?.();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
609
686
|
if (matchesKey(keyData, "right")) {
|
|
610
687
|
this.#statusMessage = undefined;
|
|
611
688
|
if (this.#model.cursorRowKind === "load-older") {
|
|
@@ -666,12 +743,169 @@ export class DebugLogViewerComponent implements Component {
|
|
|
666
743
|
|
|
667
744
|
invalidate(): void {
|
|
668
745
|
this.#frame.invalidate();
|
|
746
|
+
this.#native.clear();
|
|
669
747
|
}
|
|
670
748
|
|
|
671
749
|
dispose(): void {
|
|
672
750
|
this.#frame.dispose();
|
|
673
751
|
}
|
|
674
752
|
|
|
753
|
+
/** A `picker` sheet floats over the transcript instead of taking the screen. */
|
|
754
|
+
nativeSheet(cx: DescribeContext): boolean {
|
|
755
|
+
return cx.supports("picker");
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* The logs as a picker sheet: the filter as its query, one row per entry
|
|
760
|
+
* (message, fields, time, pid, level dot) with the session boundary as a
|
|
761
|
+
* group head, the range selection checked, the entry under the cursor
|
|
762
|
+
* pretty-printed below, and the keys' actions in the bar. Without the
|
|
763
|
+
* `picker` kind it falls back to rows.
|
|
764
|
+
*/
|
|
765
|
+
describe(cx: DescribeContext): NativeNode | null {
|
|
766
|
+
if (!cx.supports("picker")) return null;
|
|
767
|
+
const model = this.#model;
|
|
768
|
+
const query = this.#filter.getValue();
|
|
769
|
+
const cursor = this.#filter.getCursor();
|
|
770
|
+
const selected = model.getSelectedLogIndices();
|
|
771
|
+
return this.#native.get(
|
|
772
|
+
[
|
|
773
|
+
model.rows,
|
|
774
|
+
model.cursorRowIndex,
|
|
775
|
+
selected.join(","),
|
|
776
|
+
model.logCount,
|
|
777
|
+
model.isProcessFilterEnabled(),
|
|
778
|
+
model.canLoadOlder(),
|
|
779
|
+
query,
|
|
780
|
+
cursor,
|
|
781
|
+
this.#loadingOlder,
|
|
782
|
+
this.#statusMessage,
|
|
783
|
+
],
|
|
784
|
+
() => {
|
|
785
|
+
if (this.#nativeItemsCount !== model.logCount) {
|
|
786
|
+
this.#nativeItems = new Map();
|
|
787
|
+
this.#nativeItemsCount = model.logCount;
|
|
788
|
+
}
|
|
789
|
+
const items: TspPickerItem[] = [];
|
|
790
|
+
const order: (string | TspPickerGroup)[] = [];
|
|
791
|
+
const hits: Record<string, [number, number][]> = {};
|
|
792
|
+
const needle = query.toLowerCase();
|
|
793
|
+
for (const viewerRow of model.rows) {
|
|
794
|
+
if (viewerRow.kind === "warning") {
|
|
795
|
+
order.push({ group: "session", label: "This session" });
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (viewerRow.kind === "load-older") {
|
|
799
|
+
items.push({ id: OLDER_ITEM, label: "Load older entries", tone: "muted" });
|
|
800
|
+
order.push(OLDER_ITEM);
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
let item = this.#nativeItems.get(viewerRow.logIndex);
|
|
804
|
+
if (!item) {
|
|
805
|
+
item = logPickerItem(`l${viewerRow.logIndex}`, model.getRawLine(viewerRow.logIndex));
|
|
806
|
+
this.#nativeItems.set(viewerRow.logIndex, item);
|
|
807
|
+
}
|
|
808
|
+
items.push(item);
|
|
809
|
+
order.push(item.id);
|
|
810
|
+
if (needle && typeof item.label === "string") {
|
|
811
|
+
const found = pickerHits(item.label, [needle]);
|
|
812
|
+
if (found.length > 0) hits[item.id] = found;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const cursorLog = model.cursorLogIndex;
|
|
816
|
+
const preview = cursorLog === undefined ? undefined : logPreviewText(model.getRawLine(cursorLog));
|
|
817
|
+
const subtitle = [
|
|
818
|
+
`${model.visibleLogCount}/${model.logCount} entries`,
|
|
819
|
+
selected.length > 1 ? `${selected.length} selected` : undefined,
|
|
820
|
+
this.#loadingOlder ? "loading older…" : undefined,
|
|
821
|
+
]
|
|
822
|
+
.filter(part => part !== undefined)
|
|
823
|
+
.join(" · ");
|
|
824
|
+
return picker(
|
|
825
|
+
{
|
|
826
|
+
title: "Recent logs",
|
|
827
|
+
subtitle,
|
|
828
|
+
noun: "entries",
|
|
829
|
+
size: "lg",
|
|
830
|
+
layout: "rows",
|
|
831
|
+
preview: preview ? "below" : "none",
|
|
832
|
+
...pickerQuery(this.#filter),
|
|
833
|
+
placeholder: "Filter logs…",
|
|
834
|
+
columns: [
|
|
835
|
+
{ id: "pid", format: "dim", priority: 0 },
|
|
836
|
+
{ id: "time", format: "dim", priority: 1 },
|
|
837
|
+
],
|
|
838
|
+
items,
|
|
839
|
+
order,
|
|
840
|
+
hits,
|
|
841
|
+
selected:
|
|
842
|
+
model.cursorRowKind === "load-older"
|
|
843
|
+
? OLDER_ITEM
|
|
844
|
+
: cursorLog === undefined
|
|
845
|
+
? null
|
|
846
|
+
: `l${cursorLog}`,
|
|
847
|
+
current: selected.length > 1 ? selected.map(index => `l${index}`) : [],
|
|
848
|
+
total: model.logCount,
|
|
849
|
+
empty: query ? "No matching log entries" : "No log entries",
|
|
850
|
+
message: this.#statusMessage,
|
|
851
|
+
actions: [
|
|
852
|
+
pickerAction("copy", "Copy", "ctrl+c", { primary: true }),
|
|
853
|
+
pickerAction("all", "Select all", "ctrl+a"),
|
|
854
|
+
pickerAction("pid", "This process", "ctrl+p", { on: model.isProcessFilterEnabled() }),
|
|
855
|
+
pickerAction("older", "Load older", "ctrl+o", {
|
|
856
|
+
disabled: model.canLoadOlder() ? undefined : "No older log entries",
|
|
857
|
+
}),
|
|
858
|
+
CLOSE_ACTION,
|
|
859
|
+
],
|
|
860
|
+
},
|
|
861
|
+
preview && cursorLog !== undefined
|
|
862
|
+
? [{ ...code(preview.text, { lang: preview.lang, wrap: true }), key: `entry:${cursorLog}` }]
|
|
863
|
+
: undefined,
|
|
864
|
+
);
|
|
865
|
+
},
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Pointer actions run the keys' code: a row click moves the cursor there,
|
|
871
|
+
* a second click copies it (or loads older entries from that row), and the
|
|
872
|
+
* bar's buttons mirror their keys.
|
|
873
|
+
*/
|
|
874
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
875
|
+
const ev = pickerEvent(event);
|
|
876
|
+
if (!ev) return;
|
|
877
|
+
this.#statusMessage = undefined;
|
|
878
|
+
if (ev.kind === "action") {
|
|
879
|
+
switch (ev.act) {
|
|
880
|
+
case "copy":
|
|
881
|
+
this.#copySelected();
|
|
882
|
+
return;
|
|
883
|
+
case "all":
|
|
884
|
+
this.#model.selectAllVisible();
|
|
885
|
+
return;
|
|
886
|
+
case "pid":
|
|
887
|
+
this.#model.toggleProcessFilter();
|
|
888
|
+
return;
|
|
889
|
+
case "older":
|
|
890
|
+
void this.#handleLoadOlder(this.#frame.getBodyHeight() + 1);
|
|
891
|
+
return;
|
|
892
|
+
case "close":
|
|
893
|
+
this.#onExit();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const rowIndex = this.#model.rows.findIndex(viewerRow =>
|
|
899
|
+
ev.item === OLDER_ITEM
|
|
900
|
+
? viewerRow.kind === "load-older"
|
|
901
|
+
: viewerRow.kind === "log" && `l${viewerRow.logIndex}` === ev.item,
|
|
902
|
+
);
|
|
903
|
+
if (rowIndex < 0 || !this.#model.moveCursorToRow(rowIndex, false)) return;
|
|
904
|
+
if (ev.kind !== "activate") return;
|
|
905
|
+
if (ev.item === OLDER_ITEM) void this.#handleLoadOlder();
|
|
906
|
+
else this.#copySelected();
|
|
907
|
+
}
|
|
908
|
+
|
|
675
909
|
render(width: number): readonly string[] {
|
|
676
910
|
return this.#frame.render(Math.max(MIN_LOG_VIEWER_WIDTH, width));
|
|
677
911
|
}
|
|
@@ -715,7 +949,7 @@ export class DebugLogViewerComponent implements Component {
|
|
|
715
949
|
}
|
|
716
950
|
|
|
717
951
|
#controlsText(): string {
|
|
718
|
-
return `${formatKeyHint("escape")} close · ${formatKeyHint("ctrl+c")} copy · ${formatKeyHints(["up", "down"])}/wheel move · click toggle · ${formatKeyHints(["shift+up", "shift+down"])} select · ${formatKeyHints(["left", "right"])} collapse/expand · ${formatKeyHint("ctrl+a")} all · ${formatKeyHint("ctrl+o")} older · ${formatKeyHint("ctrl+p")} pid`;
|
|
952
|
+
return `${formatKeyHint("escape")} close · ${formatKeyHint("ctrl+c")} copy · ${formatKeyHints(["up", "down"])}/wheel move · ${formatKeyHints(["pageUp", "pageDown"])} page · ${formatKeyHints(["home", "end"])} first/last · click toggle · ${formatKeyHints(["shift+up", "shift+down"])} select · ${formatKeyHints(["left", "right"])} collapse/expand · ${formatKeyHint("ctrl+a")} all · ${formatKeyHint("ctrl+o")} older · ${formatKeyHint("ctrl+p")} pid`;
|
|
719
953
|
}
|
|
720
954
|
|
|
721
955
|
#filterText(): string {
|
|
@@ -23,6 +23,8 @@ import { Text } from "../../components/text";
|
|
|
23
23
|
import { ImageProtocol, NotifyProtocol, TERMINAL } from "../../terminal-capabilities";
|
|
24
24
|
import { DynamicBorder } from "../../chrome/dynamic-border";
|
|
25
25
|
import { theme } from "../../theme/theme";
|
|
26
|
+
import { ansi } from "../../native/describe";
|
|
27
|
+
import type { NativeNode } from "../../native/node";
|
|
26
28
|
|
|
27
29
|
const PNG_SIGNATURE = Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a);
|
|
28
30
|
|
|
@@ -148,13 +150,20 @@ function imageProtocolLabel(): string {
|
|
|
148
150
|
/**
|
|
149
151
|
* Deliberate exception to the normal text/data components: OSC 66 probes must
|
|
150
152
|
* reach the terminal byte-for-byte, without sanitization, wrapping, clipping,
|
|
151
|
-
* or padding that could invalidate the protocol sample.
|
|
153
|
+
* or padding that could invalidate the protocol sample. A native terminal
|
|
154
|
+
* gets the same bytes as one `ansi` block.
|
|
152
155
|
*/
|
|
153
156
|
class RawLines implements Component {
|
|
154
157
|
readonly #lines: readonly string[];
|
|
158
|
+
readonly #native: NativeNode;
|
|
155
159
|
|
|
156
160
|
constructor(lines: readonly string[]) {
|
|
157
161
|
this.#lines = lines.slice();
|
|
162
|
+
this.#native = ansi(this.#lines.join("\n"));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
describe(): NativeNode {
|
|
166
|
+
return this.#native;
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
render(): readonly string[] {
|