@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +79 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Pi's own transcript entries in the harness frame instead of a background. Pi has no hook for
|
|
2
|
+
// drawing them, so this wraps each component's render.
|
|
3
|
+
import { CHANGES_CUT, CHANGES_X, entryFrame, type EntryFrame, GLYPH } from "../kit/index.ts";
|
|
4
|
+
import {
|
|
5
|
+
BashExecutionComponent,
|
|
6
|
+
BranchSummaryMessageComponent,
|
|
7
|
+
CompactionSummaryMessageComponent,
|
|
8
|
+
CustomMessageComponent,
|
|
9
|
+
SkillInvocationMessageComponent,
|
|
10
|
+
type Theme,
|
|
11
|
+
UserMessageComponent,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { Container, Spacer, stripTerminalSequences, Text } from "@earendil-works/pi-tui";
|
|
14
|
+
|
|
15
|
+
import { type Mode, modeColor } from "../data/mode.ts";
|
|
16
|
+
import type { IconSet } from "../render/glyphs.ts";
|
|
17
|
+
import { isBlank } from "./tool-frame.ts";
|
|
18
|
+
|
|
19
|
+
/** Undefined leaves pi's entries as they are. `from` names the client a prompt came from. */
|
|
20
|
+
export type EntryPaint = () =>
|
|
21
|
+
| {
|
|
22
|
+
theme: Theme;
|
|
23
|
+
set: IconSet;
|
|
24
|
+
you: boolean;
|
|
25
|
+
from?: (text: string) => string | undefined;
|
|
26
|
+
/** The mode a message went out in. A live message has no id yet, so it goes by its text. */
|
|
27
|
+
sentIn?: (id: string | undefined, text: string) => Mode | undefined;
|
|
28
|
+
}
|
|
29
|
+
| undefined;
|
|
30
|
+
|
|
31
|
+
const YOU: Readonly<Record<IconSet, string>> = {
|
|
32
|
+
nerd: "\u{f007}", // nf-fa-user
|
|
33
|
+
unicode: GLYPH.triangle,
|
|
34
|
+
ascii: ">",
|
|
35
|
+
};
|
|
36
|
+
/** The shell integration markers pi puts on your message's first and last line. */
|
|
37
|
+
// oxlint-disable-next-line no-control-regex -- the markers are escape sequences
|
|
38
|
+
const MARKERS = /^(?:\u001b\]133;[^\u0007]*\u0007)*/;
|
|
39
|
+
// oxlint-disable-next-line no-control-regex -- colour codes are escape sequences
|
|
40
|
+
const BACKGROUND = /\u001b\[(?:48;[25];[\d;]+|49|4[0-7]|10[0-7])m/g;
|
|
41
|
+
/** Pi's bracketed label, as `[compaction]`, which the frame's top edge replaces. */
|
|
42
|
+
// oxlint-disable-next-line no-control-regex -- the label is wrapped in bold codes
|
|
43
|
+
const LABEL = /(\u001b\[1m)?\[[\w:. -]+\](\u001b\[22m)? ?/;
|
|
44
|
+
|
|
45
|
+
const STARTUP_LOADED = new Set(["Context", "Skills", "Prompts", "Extensions"]);
|
|
46
|
+
// oxlint-disable-next-line no-control-regex -- the name is wrapped in color codes
|
|
47
|
+
const STARTUP_SECTION = /^(?:\u001b\[[\d;]*m)*\[(Context|Skills|Prompts|Extensions|Skill warnings|Prompt conflicts|Extension issues|Theme conflicts)\](?:\u001b\[[\d;]*m)*\n/;
|
|
48
|
+
|
|
49
|
+
type Paint = NonNullable<ReturnType<EntryPaint>>;
|
|
50
|
+
|
|
51
|
+
interface Framing extends EntryFrame {
|
|
52
|
+
/** As `[compaction]`. */
|
|
53
|
+
label?: boolean;
|
|
54
|
+
rules?: boolean;
|
|
55
|
+
}
|
|
56
|
+
type Render = (width: number) => string[];
|
|
57
|
+
|
|
58
|
+
let installed = false;
|
|
59
|
+
let paint: EntryPaint = () => undefined;
|
|
60
|
+
|
|
61
|
+
/** Calling it again only changes where the paint comes from. */
|
|
62
|
+
export function framePiEntries(source: EntryPaint): void {
|
|
63
|
+
paint = source;
|
|
64
|
+
if (installed) return;
|
|
65
|
+
installed = true;
|
|
66
|
+
wrap(UserMessageComponent, (self, look) =>
|
|
67
|
+
look.you ? youFrame(look, fromOf(self, look), sentInOf(self, look)) : undefined,
|
|
68
|
+
);
|
|
69
|
+
wrap(CompactionSummaryMessageComponent, (_self, look) => pi(look, "compacted"));
|
|
70
|
+
wrap(BranchSummaryMessageComponent, (_self, look) => pi(look, "branch summary"));
|
|
71
|
+
wrap(SkillInvocationMessageComponent, (_self, look) => ({
|
|
72
|
+
top: look.theme.fg("muted", "skill"),
|
|
73
|
+
color: "borderMuted",
|
|
74
|
+
label: true,
|
|
75
|
+
}));
|
|
76
|
+
wrap(CustomMessageComponent, (self, look) => {
|
|
77
|
+
const own = self as unknown as { customComponent?: unknown; message: { customType: string } };
|
|
78
|
+
if (own.customComponent !== undefined) return undefined;
|
|
79
|
+
const top = look.theme.fg("muted", own.message.customType);
|
|
80
|
+
return { top, color: "borderMuted", label: true };
|
|
81
|
+
});
|
|
82
|
+
wrap(BashExecutionComponent, (self, look) => shellFrame(self, look));
|
|
83
|
+
wrap(Text, (self, look) => startupFrame(self, look));
|
|
84
|
+
liftLoadedLists();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const CARD_MIN_WIDTH = 48;
|
|
88
|
+
|
|
89
|
+
export interface StartupList {
|
|
90
|
+
name: string;
|
|
91
|
+
items: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let startupLists: readonly StartupList[] = [];
|
|
95
|
+
let startupChanged: () => void = () => {};
|
|
96
|
+
|
|
97
|
+
/** What Pi loaded, in its order, read from the list the start card takes over. */
|
|
98
|
+
export function loadedLists(): readonly StartupList[] {
|
|
99
|
+
return startupLists;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function onLoadedListsChange(listener: () => void): void {
|
|
103
|
+
startupChanged = listener;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `@scope/pi-modes@0.2.1:src` and `pi-modes/src` are both `pi-modes`. */
|
|
107
|
+
export function extensionName(label: string): string {
|
|
108
|
+
return label
|
|
109
|
+
.replace(/^@[^/]+\//, "")
|
|
110
|
+
.replace(/(?::|\/src\b).*$/, "");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A section Pi shows as one line of names, and not yet expanded with Ctrl+O. */
|
|
114
|
+
function compactSection(child: unknown): StartupList | undefined {
|
|
115
|
+
if (!(child instanceof Text) || (child as { state?: { expanded?: boolean } }).state?.expanded === true)
|
|
116
|
+
return undefined;
|
|
117
|
+
const name = STARTUP_SECTION.exec((child as unknown as { text?: string }).text ?? "")?.[1];
|
|
118
|
+
if (name === undefined || !STARTUP_LOADED.has(name)) return undefined;
|
|
119
|
+
const [, body] = stripTerminalSequences((child as unknown as { text: string }).text).split("\n");
|
|
120
|
+
const items = (body ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
121
|
+
return {
|
|
122
|
+
name: name.toLowerCase(),
|
|
123
|
+
items: name === "Extensions" ? [...new Set(items.map(extensionName))] : items,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The start card shows these lists, so Pi's own section and the spacer after it draw nothing.
|
|
129
|
+
* Ctrl+O and problem sections still draw here. Pi has no hook for this, so it wraps the
|
|
130
|
+
* container's render and leaves anything it doesn't recognize alone.
|
|
131
|
+
*/
|
|
132
|
+
function liftLoadedLists(): void {
|
|
133
|
+
const prototype = Container.prototype as unknown as { render: Render };
|
|
134
|
+
const own = prototype.render;
|
|
135
|
+
prototype.render = function render(this: Container, width: number) {
|
|
136
|
+
const first = this.children.find((child) => !(child instanceof Spacer));
|
|
137
|
+
if (paint() === undefined || width < CARD_MIN_WIDTH || !(first instanceof Text)) return own.call(this, width);
|
|
138
|
+
// Pi fills in a section's text as it draws.
|
|
139
|
+
first.render(width);
|
|
140
|
+
if (compactSection(first) === undefined) return own.call(this, width);
|
|
141
|
+
const lists: StartupList[] = [];
|
|
142
|
+
const lines: string[] = [];
|
|
143
|
+
for (const child of this.children) {
|
|
144
|
+
if (child instanceof Spacer) continue;
|
|
145
|
+
const drawn = child.render(width);
|
|
146
|
+
const list = compactSection(child);
|
|
147
|
+
if (list === undefined) lines.push(...drawn, "");
|
|
148
|
+
else lists.push(list);
|
|
149
|
+
}
|
|
150
|
+
if (JSON.stringify(lists) !== JSON.stringify(startupLists)) {
|
|
151
|
+
startupLists = lists;
|
|
152
|
+
startupChanged();
|
|
153
|
+
}
|
|
154
|
+
return lines;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Pi lists what it loaded as `[Skills]` above a plain list, and says it with `Text`. */
|
|
159
|
+
function startupFrame(self: object, look: Paint): Framing | undefined {
|
|
160
|
+
const name = STARTUP_SECTION.exec((self as { text?: string }).text ?? "")?.[1];
|
|
161
|
+
if (name === undefined) return undefined;
|
|
162
|
+
const problem = !STARTUP_LOADED.has(name);
|
|
163
|
+
return {
|
|
164
|
+
top: look.theme.fg(problem ? "warning" : "muted", name.toLowerCase()),
|
|
165
|
+
color: problem ? "warning" : "borderMuted",
|
|
166
|
+
label: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Asked once per message: the client is known before pi draws the prompt, and never changes. */
|
|
171
|
+
const sentFrom = new WeakMap<object, string | undefined>();
|
|
172
|
+
|
|
173
|
+
function fromOf(self: object, look: Paint): string | undefined {
|
|
174
|
+
if (!sentFrom.has(self)) sentFrom.set(self, look.from?.((self as { text?: string }).text ?? ""));
|
|
175
|
+
return sentFrom.get(self);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const sentModes = new WeakMap<object, Mode | undefined>();
|
|
179
|
+
|
|
180
|
+
function sentInOf(self: object, look: Paint): Mode | undefined {
|
|
181
|
+
if (sentModes.has(self)) return sentModes.get(self);
|
|
182
|
+
const { messageId, text } = self as { messageId?: string; text?: string };
|
|
183
|
+
const mode = look.sentIn?.(messageId, text ?? "");
|
|
184
|
+
// A later message with the same text may go out in another mode, so a found mode sticks to this one.
|
|
185
|
+
if (mode !== undefined || messageId !== undefined) sentModes.set(self, mode);
|
|
186
|
+
return mode;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Plan is the same color as a plain message, so the name says it too. */
|
|
190
|
+
function youFrame(look: Paint, from: string | undefined, mode: Mode | undefined): Framing {
|
|
191
|
+
const color = (mode && modeColor(mode)) ?? "accent";
|
|
192
|
+
const named = mode === undefined || mode === "normal" ? "" : ` ${look.theme.fg(color, mode)}`;
|
|
193
|
+
const you = `${look.theme.fg(color, YOU[look.set])} ${look.theme.fg("muted", "you")}${named}`;
|
|
194
|
+
const top = from === undefined ? you : `${you} ${look.theme.fg("dim", `from ${from}`)}`;
|
|
195
|
+
return { top, color };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function pi(look: Paint, what: string): Framing {
|
|
199
|
+
return { top: look.theme.fg("muted", `${GLYPH.pi} ${what}`), color: "borderMuted", label: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface Shell {
|
|
203
|
+
status?: "running" | "complete" | "cancelled" | "error";
|
|
204
|
+
exitCode?: number;
|
|
205
|
+
colorKey?: "bashMode" | "dim";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function shellFrame(self: object, look: Paint): Framing {
|
|
209
|
+
const { status, exitCode, colorKey = "bashMode" } = self as Shell;
|
|
210
|
+
const { theme } = look;
|
|
211
|
+
const top = `${theme.fg(colorKey, "!")} ${theme.fg("muted", "shell")}`;
|
|
212
|
+
return { top, bottom: shellPhase(theme, status, exitCode), color: colorKey, rules: true };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function shellPhase(theme: Theme, status: Shell["status"], exitCode: number | undefined): string {
|
|
216
|
+
switch (status) {
|
|
217
|
+
case "running":
|
|
218
|
+
return theme.fg("muted", "running");
|
|
219
|
+
case "cancelled":
|
|
220
|
+
return theme.fg("warning", "cancelled");
|
|
221
|
+
case "error":
|
|
222
|
+
return theme.fg("error", `${GLYPH.cross} exit ${exitCode ?? "?"}`);
|
|
223
|
+
default:
|
|
224
|
+
return theme.fg("success", `${GLYPH.check} done`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function wrap(
|
|
229
|
+
component: { prototype: object },
|
|
230
|
+
frameOf: (self: object, look: Paint) => Framing | undefined,
|
|
231
|
+
): void {
|
|
232
|
+
const prototype = component.prototype as { render: Render };
|
|
233
|
+
const own = prototype.render;
|
|
234
|
+
prototype.render = function render(this: object, width: number) {
|
|
235
|
+
const look = paint();
|
|
236
|
+
const frame = look === undefined || width < CHANGES_X * 2 ? undefined : frameOf(this, look);
|
|
237
|
+
if (look === undefined || frame === undefined) return own.call(this, width);
|
|
238
|
+
const source = own.call(this, width - 2);
|
|
239
|
+
// Pi caches its own lines, so the frame redraws only when they, the width or the paint change.
|
|
240
|
+
const known = drawnBefore.get(this);
|
|
241
|
+
const key = `${width}\u0000${frame.top}\u0000${frame.bottom}\u0000${frame.color}`;
|
|
242
|
+
if (known?.key === key && known.theme === look.theme && sameLines(known.source, source))
|
|
243
|
+
return known.lines;
|
|
244
|
+
const lines = framed(source, width, look.theme, frame);
|
|
245
|
+
drawnBefore.set(this, { key, theme: look.theme, source, lines });
|
|
246
|
+
return lines;
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface Drawn {
|
|
251
|
+
key: string;
|
|
252
|
+
theme: Theme;
|
|
253
|
+
source: readonly string[];
|
|
254
|
+
lines: string[];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const drawnBefore = new WeakMap<object, Drawn>();
|
|
258
|
+
|
|
259
|
+
function sameLines(a: readonly string[], b: readonly string[]): boolean {
|
|
260
|
+
if (a === b) return true;
|
|
261
|
+
if (a.length !== b.length) return false;
|
|
262
|
+
for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Two columns narrower, with a column of padding each side. */
|
|
267
|
+
export function framed(
|
|
268
|
+
body: readonly string[],
|
|
269
|
+
width: number,
|
|
270
|
+
theme: Theme,
|
|
271
|
+
framing: Framing,
|
|
272
|
+
): string[] {
|
|
273
|
+
const { label, rules, ...frame } = framing;
|
|
274
|
+
const first = body[0]?.match(MARKERS)?.[0] ?? "";
|
|
275
|
+
const last = body.at(-1)?.match(MARKERS)?.[0] ?? "";
|
|
276
|
+
let lines = body.map((line) => line.replace(MARKERS, "").replace(BACKGROUND, ""));
|
|
277
|
+
if (rules) {
|
|
278
|
+
lines = lines.filter((line) => !/^\u2500+$/.test(stripTerminalSequences(line).trim()));
|
|
279
|
+
const gap = lines.findIndex((line, index) => index > 0 && isBlank(line));
|
|
280
|
+
if (gap > 0 && gap < lines.length - 1) lines[gap] = CHANGES_CUT;
|
|
281
|
+
}
|
|
282
|
+
const trim = () => {
|
|
283
|
+
while (lines.length > 1 && isBlank(lines[0] ?? "")) lines.shift();
|
|
284
|
+
while (lines.length > 1 && isBlank(lines.at(-1) ?? "")) lines.pop();
|
|
285
|
+
};
|
|
286
|
+
trim();
|
|
287
|
+
if (label && lines[0] !== undefined) {
|
|
288
|
+
// Alone on its line it goes with the line; otherwise only the label goes, as `[skill] name`.
|
|
289
|
+
const alone = /^\[[\w:. -]+\]$/.test(stripTerminalSequences(lines[0]).trim());
|
|
290
|
+
if (alone) lines.shift();
|
|
291
|
+
else lines[0] = lines[0].replace(LABEL, "$2");
|
|
292
|
+
trim();
|
|
293
|
+
}
|
|
294
|
+
const drawn = entryFrame(lines, width, theme, { ...frame, pad: 0 });
|
|
295
|
+
drawn[0] = first + drawn[0];
|
|
296
|
+
drawn[drawn.length - 1] = last + drawn.at(-1);
|
|
297
|
+
return drawn;
|
|
298
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// What each look setting changes, drawn under the list in Alt+S.
|
|
2
|
+
import { entryFrame, GLYPH, type Json } from "../kit/index.ts";
|
|
3
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
|
|
6
|
+
import { BOXES, border, type FrameStyle } from "../render/frame.ts";
|
|
7
|
+
import { type IconMode, resolveIcons } from "../render/glyphs.ts";
|
|
8
|
+
import { stateMark, toolIcon } from "../render/tools.ts";
|
|
9
|
+
|
|
10
|
+
const WIDTH = 56;
|
|
11
|
+
const TOOLS = [
|
|
12
|
+
"bash",
|
|
13
|
+
"read",
|
|
14
|
+
"edit",
|
|
15
|
+
"grep",
|
|
16
|
+
"web_fetch",
|
|
17
|
+
"ask_questions",
|
|
18
|
+
"peers",
|
|
19
|
+
"memory_write",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function iconsPreview(value: Json, theme: Theme): string[] {
|
|
23
|
+
const set = resolveIcons(value as IconMode).set;
|
|
24
|
+
const tools = TOOLS.map(
|
|
25
|
+
(tool) => `${theme.fg("toolTitle", toolIcon(tool, set))} ${theme.fg("muted", tool)}`,
|
|
26
|
+
);
|
|
27
|
+
const phases = (["writing", "waiting", "running", "done", "failed"] as const).map(
|
|
28
|
+
(phase) => `${theme.fg("accent", stateMark(phase, set, 0))} ${theme.fg("muted", phase)}`,
|
|
29
|
+
);
|
|
30
|
+
return [tools.slice(0, 4).join(" "), tools.slice(4).join(" "), "", phases.join(" ")];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function framePreview(value: Json, theme: Theme, width: number): string[] {
|
|
34
|
+
const style = value as FrameStyle | "off";
|
|
35
|
+
const room = Math.min(WIDTH, width);
|
|
36
|
+
const line = (color: "borderAccent", text: string) => theme.fg(color, text);
|
|
37
|
+
if (style === "off")
|
|
38
|
+
return [
|
|
39
|
+
theme.fg("dim", "pi's editor, as pi draws it, with the edges' information in the footer"),
|
|
40
|
+
];
|
|
41
|
+
const box = BOXES[style];
|
|
42
|
+
// Drawn with the frame's own border, so it cannot drift from the editor.
|
|
43
|
+
const edge = (which: "top" | "bottom", label: string, rest: string) =>
|
|
44
|
+
border(room, [{ text: label, priority: 1 }], [{ text: rest, priority: 2 }], {
|
|
45
|
+
box,
|
|
46
|
+
edge: which,
|
|
47
|
+
paint: (text) => line("borderAccent", text),
|
|
48
|
+
separator: " ",
|
|
49
|
+
});
|
|
50
|
+
const side = style === "line" ? " " : line("borderAccent", box.vertical);
|
|
51
|
+
return [
|
|
52
|
+
edge("top", theme.fg("warning", "main"), theme.fg("muted", "~/project")),
|
|
53
|
+
`${side} ${theme.fg("accent", GLYPH.cursor)}${" ".repeat(Math.max(0, room - 4))}${side}`,
|
|
54
|
+
edge("bottom", theme.fg("text", "Opus 5.5"), theme.fg("muted", "ctx 34%")),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function userFramePreview(value: Json, theme: Theme, width: number): string[] {
|
|
59
|
+
const room = Math.min(WIDTH, width);
|
|
60
|
+
const text = theme.fg("text", "commit with a semantic message and push");
|
|
61
|
+
if (value === true)
|
|
62
|
+
return entryFrame([text], room, theme, {
|
|
63
|
+
top: `${theme.fg("accent", GLYPH.triangle)} ${theme.fg("muted", "you")}`,
|
|
64
|
+
color: "accent",
|
|
65
|
+
});
|
|
66
|
+
const blank = theme.bg("userMessageBg", " ".repeat(room));
|
|
67
|
+
const body = theme.bg(
|
|
68
|
+
"userMessageBg",
|
|
69
|
+
` ${text}${" ".repeat(Math.max(0, room - 1 - visibleWidth(text)))}`,
|
|
70
|
+
);
|
|
71
|
+
return [blank, body, blank];
|
|
72
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Messages sent while the agent works, in a dashed frame above the editor. Pi has no hook for
|
|
2
|
+
// drawing them, so this wraps its method.
|
|
3
|
+
import { CUT, drawnByWidth, CHANGES_X, entryFrame } from "../kit/index.ts";
|
|
4
|
+
import { InteractiveMode, type Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
|
+
|
|
7
|
+
/** Undefined leaves pi's list as it is. */
|
|
8
|
+
export type QueuePaint = () => Theme | undefined;
|
|
9
|
+
|
|
10
|
+
interface Queued {
|
|
11
|
+
steering: readonly string[];
|
|
12
|
+
followUp: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Host {
|
|
16
|
+
pendingMessagesContainer: { clear(): void; addChild(component: Component): void };
|
|
17
|
+
getAllQueuedMessages(): Queued;
|
|
18
|
+
getAppKeyDisplay(action: string): string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let installed = false;
|
|
22
|
+
let paint: QueuePaint = () => undefined;
|
|
23
|
+
|
|
24
|
+
export function frameQueue(source: QueuePaint): void {
|
|
25
|
+
paint = source;
|
|
26
|
+
if (installed) return;
|
|
27
|
+
installed = true;
|
|
28
|
+
const prototype = InteractiveMode.prototype as unknown as {
|
|
29
|
+
updatePendingMessagesDisplay(): void;
|
|
30
|
+
};
|
|
31
|
+
const own = prototype.updatePendingMessagesDisplay;
|
|
32
|
+
// Same guard as `peers/src/cleared.ts`: fail loudly if pi renames this method.
|
|
33
|
+
if (typeof own !== "function")
|
|
34
|
+
throw new Error("pi has no InteractiveMode.updatePendingMessagesDisplay: write to the pi");
|
|
35
|
+
prototype.updatePendingMessagesDisplay = function update(this: Host) {
|
|
36
|
+
const theme = paint();
|
|
37
|
+
if (theme === undefined) return own.call(this);
|
|
38
|
+
const queued = this.getAllQueuedMessages();
|
|
39
|
+
this.pendingMessagesContainer.clear();
|
|
40
|
+
if (queued.steering.length + queued.followUp.length === 0) return;
|
|
41
|
+
const key = this.getAppKeyDisplay("app.message.dequeue");
|
|
42
|
+
this.pendingMessagesContainer.addChild(queueBlock(queued, key, theme));
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const MODES = [
|
|
47
|
+
["steer", "steering"],
|
|
48
|
+
["next", "followUp"],
|
|
49
|
+
] as const;
|
|
50
|
+
const LABEL = Math.max(...MODES.map(([label]) => label.length));
|
|
51
|
+
|
|
52
|
+
export function queueBlock(queued: Queued, key: string, theme: Theme): Component {
|
|
53
|
+
return drawnByWidth((width) => {
|
|
54
|
+
const room = Math.max(1, width - CHANGES_X - LABEL - 2);
|
|
55
|
+
const rows = MODES.flatMap(([label, mode]) =>
|
|
56
|
+
queued[mode].map((text) => {
|
|
57
|
+
const color = label === "steer" ? "accent" : "muted";
|
|
58
|
+
return `${theme.fg(color, label.padEnd(LABEL))} ${theme.fg("text", oneLine(text, room))}`;
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
return entryFrame(rows, width, theme, {
|
|
62
|
+
top: theme.fg("dim", "queued"),
|
|
63
|
+
bottom: `${theme.fg("muted", key)} ${theme.fg("dim", "to edit")}`,
|
|
64
|
+
color: "borderMuted",
|
|
65
|
+
dashed: true,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function oneLine(text: string, room: number): string {
|
|
71
|
+
const lines = text.trim().split("\n");
|
|
72
|
+
const first = lines[0] ?? "";
|
|
73
|
+
if (lines.length === 1 && visibleWidth(first) <= room) return first;
|
|
74
|
+
return `${truncateToWidth(first, Math.max(1, room - CUT.length), "")}${CUT}`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Snapshot } from "../data/snapshot.ts";
|
|
2
|
+
import type { Piece } from "../render/fit.ts";
|
|
3
|
+
import type { FrameStyle } from "../render/frame.ts";
|
|
4
|
+
import type { Glyphs } from "../render/glyphs.ts";
|
|
5
|
+
import type { Paint } from "../render/paint.ts";
|
|
6
|
+
import { renderSegments, type SegmentId, type SegmentOptions } from "../render/segments.ts";
|
|
7
|
+
import type { CursorStyle } from "../settings.ts";
|
|
8
|
+
|
|
9
|
+
export type SlotName =
|
|
10
|
+
| "strip"
|
|
11
|
+
| "stripRight"
|
|
12
|
+
| "topLeft"
|
|
13
|
+
| "topRight"
|
|
14
|
+
| "bottomLeft"
|
|
15
|
+
| "bottomRight"
|
|
16
|
+
| "below"
|
|
17
|
+
| "belowRight";
|
|
18
|
+
|
|
19
|
+
export interface Screen {
|
|
20
|
+
snapshot(): Snapshot;
|
|
21
|
+
glyphs(): Glyphs;
|
|
22
|
+
paint(frame?: (text: string) => string): Paint;
|
|
23
|
+
separator(): string;
|
|
24
|
+
options(): SegmentOptions;
|
|
25
|
+
slot(name: SlotName): readonly SegmentId[];
|
|
26
|
+
frameStyle(): FrameStyle | "off";
|
|
27
|
+
/** Undefined leaves the frame in pi's own color. */
|
|
28
|
+
frameColor(): ((text: string) => string) | undefined;
|
|
29
|
+
cursor(): CursorStyle;
|
|
30
|
+
decorate(line: string): string;
|
|
31
|
+
/** The color of a `/command` or `!command` at the start of the prompt. */
|
|
32
|
+
promptColor(kind: "command" | "bash"): (text: string) => string;
|
|
33
|
+
/** What was typed in this folder's sessions, newest first. */
|
|
34
|
+
pastPrompts(): string[];
|
|
35
|
+
skills(): boolean;
|
|
36
|
+
/** Changes when another package reports something new. */
|
|
37
|
+
statusesKey(): string;
|
|
38
|
+
/** A short message for the bottom line, in place of the session's numbers. */
|
|
39
|
+
notice(): string | undefined;
|
|
40
|
+
/** Ctrl+C with the editor focused, typed text included so it can be cleared. */
|
|
41
|
+
interrupt(clearEditor: () => void): void;
|
|
42
|
+
/** Compared by anything that keeps a drawing, so a change does not wait for the next keystroke. */
|
|
43
|
+
revision(): number;
|
|
44
|
+
bump(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function slotPieces(
|
|
48
|
+
screen: Screen,
|
|
49
|
+
name: SlotName,
|
|
50
|
+
snapshot: Snapshot,
|
|
51
|
+
paint: Paint,
|
|
52
|
+
): Piece[] {
|
|
53
|
+
return renderSegments(screen.slot(name), {
|
|
54
|
+
snapshot,
|
|
55
|
+
glyphs: screen.glyphs(),
|
|
56
|
+
paint,
|
|
57
|
+
options: screen.options(),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
import type { AnsweredMessage } from "../render/timing.ts";
|
|
5
|
+
|
|
6
|
+
export type TimingSource = (message: AnsweredMessage, width: number) => string | undefined;
|
|
7
|
+
|
|
8
|
+
const PAD = 1;
|
|
9
|
+
|
|
10
|
+
let source: TimingSource = () => undefined;
|
|
11
|
+
let installed = false;
|
|
12
|
+
|
|
13
|
+
interface Drawn {
|
|
14
|
+
source: readonly string[];
|
|
15
|
+
line: string;
|
|
16
|
+
width: number;
|
|
17
|
+
lines: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Every message in the session draws again on each keypress, so a long one must not redo this work. */
|
|
21
|
+
const drawnBefore = new WeakMap<object, Drawn>();
|
|
22
|
+
|
|
23
|
+
function sameLines(a: readonly string[], b: readonly string[]): boolean {
|
|
24
|
+
return a === b || (a.length === b.length && a.every((line, index) => line === b[index]));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pi has no hook for what follows a response, so this adds a line to its drawing. It asks on each
|
|
29
|
+
* drawing, so the line keeps up while the response is written. Calling again changes the source.
|
|
30
|
+
*/
|
|
31
|
+
export function showTimings(next: TimingSource): void {
|
|
32
|
+
source = next;
|
|
33
|
+
if (installed) return;
|
|
34
|
+
installed = true;
|
|
35
|
+
const prototype = AssistantMessageComponent.prototype;
|
|
36
|
+
const render = prototype.render;
|
|
37
|
+
prototype.render = function (this: AssistantMessageComponent, width: number): string[] {
|
|
38
|
+
const lines = render.call(this, width);
|
|
39
|
+
const message = (this as unknown as { lastMessage?: AnsweredMessage }).lastMessage;
|
|
40
|
+
const line = message === undefined ? undefined : source(message, Math.max(1, width - PAD));
|
|
41
|
+
if (line === undefined) return lines;
|
|
42
|
+
const known = drawnBefore.get(this);
|
|
43
|
+
if (known?.line === line && known.width === width && sameLines(known.source, lines)) return known.lines;
|
|
44
|
+
const drawn = [...lines, truncateToWidth(`${" ".repeat(PAD)}${line}`, width)];
|
|
45
|
+
drawnBefore.set(this, { source: lines, line, width, lines: drawn });
|
|
46
|
+
return drawn;
|
|
47
|
+
};
|
|
48
|
+
}
|