@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,362 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { CUT, GLYPH } from "../kit/index.ts";
|
|
4
|
+
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
|
+
|
|
7
|
+
import type { GithubState } from "../data/github.ts";
|
|
8
|
+
import { modeOf } from "../data/mode.ts";
|
|
9
|
+
import { type LastVisit, since, type StartNote, startNotes } from "../data/start-notes.ts";
|
|
10
|
+
import { BOXES, border } from "../render/frame.ts";
|
|
11
|
+
import { keyLabel, SETTINGS_KEY } from "../render/keys.ts";
|
|
12
|
+
import { EFFORT_LEVELS, type Paint } from "../render/paint.ts";
|
|
13
|
+
import { styledText } from "../render/safe.ts";
|
|
14
|
+
import { renderSegments, type SegmentId } from "../render/segments.ts";
|
|
15
|
+
import type { HeaderStyle } from "../settings.ts";
|
|
16
|
+
import type { StartupList } from "./pi-entries.ts";
|
|
17
|
+
import type { Screen } from "./screen.ts";
|
|
18
|
+
|
|
19
|
+
/** The π the start card draws, and the docs draw the project's icon from. */
|
|
20
|
+
export const PIXELS = [
|
|
21
|
+
"..............",
|
|
22
|
+
".############.",
|
|
23
|
+
"##############",
|
|
24
|
+
"...##....##...",
|
|
25
|
+
"...##....##...",
|
|
26
|
+
"...##....##...",
|
|
27
|
+
"...##....##...",
|
|
28
|
+
"...##....##...",
|
|
29
|
+
"..##.....###..",
|
|
30
|
+
".##.......####",
|
|
31
|
+
];
|
|
32
|
+
const LOGO = halfBlocks(PIXELS);
|
|
33
|
+
export const LOGO_RAMP = EFFORT_LEVELS.slice(1);
|
|
34
|
+
const LOGO_COLUMN = 20;
|
|
35
|
+
const MIN_RIGHT = 34;
|
|
36
|
+
const MIN_CARD = 48;
|
|
37
|
+
|
|
38
|
+
export interface LoadedCounts {
|
|
39
|
+
tools: number;
|
|
40
|
+
skills: number;
|
|
41
|
+
prompts: number;
|
|
42
|
+
extensions: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface HeaderSource {
|
|
46
|
+
style(): HeaderStyle;
|
|
47
|
+
counts(): LoadedCounts;
|
|
48
|
+
/** Undefined for the first session here. */
|
|
49
|
+
lastVisit(): LastVisit | undefined;
|
|
50
|
+
/** Undefined while it is still being asked. */
|
|
51
|
+
github?(): GithubState | undefined;
|
|
52
|
+
/** What Pi loaded, one row each. */
|
|
53
|
+
loaded?(): readonly StartupList[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface Hint {
|
|
57
|
+
key: string;
|
|
58
|
+
does: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Once per launch, one row of the art every step. */
|
|
62
|
+
const RISE_STEP_MS = 70;
|
|
63
|
+
const NOTES = 6;
|
|
64
|
+
const REDRAW_MS = 1000;
|
|
65
|
+
const MODE_KEY = "shift+tab";
|
|
66
|
+
/** Read once from the package that ships it. */
|
|
67
|
+
export const PACKAGE_VERSION = packageVersion();
|
|
68
|
+
|
|
69
|
+
export class HeaderComponent implements Component {
|
|
70
|
+
readonly #screen: Screen;
|
|
71
|
+
readonly #source: HeaderSource;
|
|
72
|
+
/** Without a terminal to redraw it, it stands from the start. */
|
|
73
|
+
readonly #born: number;
|
|
74
|
+
|
|
75
|
+
constructor(screen: Screen, source: HeaderSource, tui?: { requestRender(): void }) {
|
|
76
|
+
this.#screen = screen;
|
|
77
|
+
this.#source = source;
|
|
78
|
+
this.#born = tui === undefined ? 0 : Date.now();
|
|
79
|
+
if (tui === undefined) return;
|
|
80
|
+
for (let row = 1; row <= LOGO.length; row++)
|
|
81
|
+
setTimeout(() => tui.requestRender(), row * RISE_STEP_MS).unref?.();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Kept for a moment: the card changes rarely and pi redraws on every keystroke. */
|
|
85
|
+
#drawn:
|
|
86
|
+
| {
|
|
87
|
+
width: number;
|
|
88
|
+
at: number;
|
|
89
|
+
github: GithubState | undefined;
|
|
90
|
+
loaded: readonly StartupList[] | undefined;
|
|
91
|
+
lines: string[];
|
|
92
|
+
}
|
|
93
|
+
| undefined;
|
|
94
|
+
|
|
95
|
+
render(width: number): string[] {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const github = this.#source.github?.();
|
|
98
|
+
const loaded = this.#source.loaded?.();
|
|
99
|
+
const risen = now - this.#born > LOGO.length * RISE_STEP_MS;
|
|
100
|
+
const drawn = this.#drawn;
|
|
101
|
+
// GitHub answers after the card is up, and that must not wait for the next keystroke.
|
|
102
|
+
if (
|
|
103
|
+
risen &&
|
|
104
|
+
drawn?.width === width &&
|
|
105
|
+
drawn.github === github &&
|
|
106
|
+
drawn.loaded === loaded &&
|
|
107
|
+
now - drawn.at < REDRAW_MS
|
|
108
|
+
)
|
|
109
|
+
return drawn.lines;
|
|
110
|
+
const notes = startNotes(
|
|
111
|
+
this.#screen.snapshot(),
|
|
112
|
+
PACKAGE_VERSION,
|
|
113
|
+
this.#source.lastVisit(),
|
|
114
|
+
github,
|
|
115
|
+
);
|
|
116
|
+
// A pull request's title, a workflow's name and the folder come from outside.
|
|
117
|
+
const lines = (
|
|
118
|
+
width < MIN_CARD || this.#source.style() === "compact"
|
|
119
|
+
? this.#compact(width, notes)
|
|
120
|
+
: this.#card(width, notes)
|
|
121
|
+
).map(styledText);
|
|
122
|
+
this.#drawn = { width, at: now, github, loaded, lines };
|
|
123
|
+
return lines;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
invalidate(): void {
|
|
127
|
+
this.#drawn = undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#card(width: number, notes: readonly StartNote[]): string[] {
|
|
131
|
+
const screen = this.#screen;
|
|
132
|
+
const paint = screen.paint();
|
|
133
|
+
const box = BOXES.rounded;
|
|
134
|
+
const frame = paint.frame;
|
|
135
|
+
const inner = width - 4;
|
|
136
|
+
const withLogo = inner - LOGO_COLUMN - 2 >= MIN_RIGHT;
|
|
137
|
+
const rightWidth = withLogo ? inner - LOGO_COLUMN - 2 : inner;
|
|
138
|
+
const snapshot = screen.snapshot();
|
|
139
|
+
const last = this.#source.lastVisit();
|
|
140
|
+
|
|
141
|
+
const name = this.#name(paint);
|
|
142
|
+
const right = [
|
|
143
|
+
"",
|
|
144
|
+
...(name === "" ? [] : [name]),
|
|
145
|
+
this.#segments(["path", "branch"], paint),
|
|
146
|
+
this.#segments(["model", "effort"], paint),
|
|
147
|
+
"",
|
|
148
|
+
...this.#notes(notes, paint, last, snapshot.now),
|
|
149
|
+
...this.#loaded(paint, rightWidth),
|
|
150
|
+
];
|
|
151
|
+
const left = ["", ...this.#logo(paint), ""];
|
|
152
|
+
|
|
153
|
+
const rows = Math.max(left.length, right.length);
|
|
154
|
+
const lines = [
|
|
155
|
+
border(
|
|
156
|
+
width,
|
|
157
|
+
[{ text: this.#title(paint), priority: 1 }],
|
|
158
|
+
[{ text: paint.dim(`pi ${snapshot.version}`), priority: 2 }],
|
|
159
|
+
{ box, edge: "top", paint: frame, separator: " " },
|
|
160
|
+
),
|
|
161
|
+
];
|
|
162
|
+
for (let row = 0; row < rows; row++) {
|
|
163
|
+
const content = withLogo
|
|
164
|
+
? `${center(left[row] ?? "", LOGO_COLUMN)} ${pad(right[row] ?? "", rightWidth)}`
|
|
165
|
+
: pad(right[row] ?? "", rightWidth);
|
|
166
|
+
lines.push(`${frame(box.vertical)} ${content} ${frame(box.vertical)}`);
|
|
167
|
+
}
|
|
168
|
+
lines.push(
|
|
169
|
+
border(
|
|
170
|
+
width,
|
|
171
|
+
[
|
|
172
|
+
// What waits on you outlasts the key hints when the edge runs out of room.
|
|
173
|
+
{ text: this.#closing(notes, paint, last, snapshot.now), priority: 100 },
|
|
174
|
+
...this.#hintPieces(paint),
|
|
175
|
+
],
|
|
176
|
+
[],
|
|
177
|
+
{
|
|
178
|
+
box,
|
|
179
|
+
edge: "bottom",
|
|
180
|
+
paint: frame,
|
|
181
|
+
separator: " ",
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
lines.push("");
|
|
186
|
+
// Every line is drawn to the width already, and measuring is cheaper than cutting.
|
|
187
|
+
return lines.map((line) =>
|
|
188
|
+
visibleWidth(line) > width ? truncateToWidth(line, width, "") : line,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#logo(paint: Paint): string[] {
|
|
193
|
+
const risen = Math.floor((Date.now() - this.#born) / RISE_STEP_MS) + 1;
|
|
194
|
+
return LOGO.map((row, index) =>
|
|
195
|
+
LOGO.length - index > risen
|
|
196
|
+
? " ".repeat(row.length)
|
|
197
|
+
: paint.effort(LOGO_RAMP[index] ?? "max", row),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#notes(
|
|
202
|
+
notes: readonly StartNote[],
|
|
203
|
+
paint: Paint,
|
|
204
|
+
last: LastVisit | undefined,
|
|
205
|
+
now: number,
|
|
206
|
+
): string[] {
|
|
207
|
+
if (notes.length === 0)
|
|
208
|
+
return [
|
|
209
|
+
paint.muted(
|
|
210
|
+
last === undefined
|
|
211
|
+
? "First session in this folder."
|
|
212
|
+
: `Nothing new since ${since(last.at, now)}.`,
|
|
213
|
+
),
|
|
214
|
+
];
|
|
215
|
+
const shown = notes.slice(0, NOTES).map((note) => {
|
|
216
|
+
const detail = note.detail === undefined ? "" : ` ${paint.dim(note.detail)}`;
|
|
217
|
+
return `${paint.raw(note.color, note.mark)} ${paint.text(note.text)}${detail}`;
|
|
218
|
+
});
|
|
219
|
+
if (notes.length > NOTES) shown.push(paint.dim(`and ${notes.length - NOTES} more`));
|
|
220
|
+
return shown;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#loaded(paint: Paint, width: number): string[] {
|
|
224
|
+
const lists = (this.#source.loaded?.() ?? []).filter((list) => list.items.length > 0);
|
|
225
|
+
if (lists.length === 0) return [];
|
|
226
|
+
const label = Math.max(...lists.map((list) => list.name.length)) + 2;
|
|
227
|
+
return [
|
|
228
|
+
"",
|
|
229
|
+
...lists.map(
|
|
230
|
+
({ name, items }) =>
|
|
231
|
+
`${paint.dim(name.padEnd(label))}${paint.muted(fitNames(items, width - label))}`,
|
|
232
|
+
),
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#closing(
|
|
237
|
+
notes: readonly StartNote[],
|
|
238
|
+
paint: Paint,
|
|
239
|
+
last: LastVisit | undefined,
|
|
240
|
+
now: number,
|
|
241
|
+
): string {
|
|
242
|
+
if (notes.length === 0) return paint.italic(paint.dim("ready when you are"));
|
|
243
|
+
const things = `${notes.length} thing${notes.length === 1 ? "" : "s"}`;
|
|
244
|
+
return paint.raw(
|
|
245
|
+
"warning",
|
|
246
|
+
last === undefined ? things : `${things} since ${since(last.at, now)}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#hintPieces(paint: Paint): { text: string; priority: number }[] {
|
|
251
|
+
const snapshot = this.#screen.snapshot();
|
|
252
|
+
return hints(modeOf(snapshot.statuses) !== undefined)
|
|
253
|
+
.map((hint, index) => ({
|
|
254
|
+
text: `${paint.role("brand", hint.key)} ${paint.dim(hint.does)}`,
|
|
255
|
+
priority: 3 + index,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#compact(width: number, notes: readonly StartNote[]): string[] {
|
|
260
|
+
const paint = this.#screen.paint();
|
|
261
|
+
const separator = paint.dim(this.#screen.separator());
|
|
262
|
+
const first = [
|
|
263
|
+
this.#title(paint),
|
|
264
|
+
this.#name(paint),
|
|
265
|
+
this.#segments(["model", "effort", "path", "branch"], paint),
|
|
266
|
+
]
|
|
267
|
+
.filter((part) => part !== "")
|
|
268
|
+
.join(separator);
|
|
269
|
+
const last = this.#source.lastVisit();
|
|
270
|
+
return [first, ...this.#notes(notes, paint, last, this.#screen.snapshot().now), ""].map(
|
|
271
|
+
(line) => {
|
|
272
|
+
const padded = ` ${line}`;
|
|
273
|
+
return visibleWidth(padded) > width ? truncateToWidth(padded, width, CUT) : padded;
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#title(paint: Paint): string {
|
|
279
|
+
const glyph = paint.bold(paint.role("brand", GLYPH.pi));
|
|
280
|
+
return `${glyph}${paint.bold("-frame")} ${paint.dim(PACKAGE_VERSION)}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The name the rooms and the remote page use for this session, with pi's /name beside it. */
|
|
284
|
+
#name(paint: Paint): string {
|
|
285
|
+
const word = this.#segments(["me"], paint);
|
|
286
|
+
const { sessionName } = this.#screen.snapshot();
|
|
287
|
+
if (word === "" || sessionName === undefined || sessionName === "") return word;
|
|
288
|
+
return `${word} ${paint.dim(sessionName)}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#segments(ids: readonly SegmentId[], paint: Paint): string {
|
|
292
|
+
const pieces = renderSegments(ids, {
|
|
293
|
+
snapshot: this.#screen.snapshot(),
|
|
294
|
+
glyphs: this.#screen.glyphs(),
|
|
295
|
+
paint,
|
|
296
|
+
options: this.#screen.options(),
|
|
297
|
+
});
|
|
298
|
+
return pieces.map((piece) => piece.text).join(paint.dim(this.#screen.separator()));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function packageVersion(): string {
|
|
303
|
+
try {
|
|
304
|
+
const file = new URL("../../package.json", import.meta.url);
|
|
305
|
+
return (JSON.parse(readFileSync(file, "utf8")) as { version?: string }).version ?? "";
|
|
306
|
+
} catch {
|
|
307
|
+
return "";
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** With pi-modes loaded, Shift+Tab changes the mode, whatever pi's own binding says. */
|
|
312
|
+
export function hints(modes: boolean): Hint[] {
|
|
313
|
+
return [
|
|
314
|
+
{ key: "/", does: "commands" },
|
|
315
|
+
{ key: keyText("app.model.select") || "/model", does: "model" },
|
|
316
|
+
...(modes ? [{ key: MODE_KEY, does: "mode" }] : []),
|
|
317
|
+
{ key: keyLabel(SETTINGS_KEY), does: "settings" },
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** As many names as fit, then how many did not. */
|
|
322
|
+
export function fitNames(names: readonly string[], width: number): string {
|
|
323
|
+
let fitted = names.join(", ");
|
|
324
|
+
if (visibleWidth(fitted) <= width) return fitted;
|
|
325
|
+
for (let shown = names.length - 1; shown > 0; shown--) {
|
|
326
|
+
fitted = `${names.slice(0, shown).join(", ")}, +${names.length - shown}`;
|
|
327
|
+
if (visibleWidth(fitted) <= width) return fitted;
|
|
328
|
+
}
|
|
329
|
+
return truncateToWidth(`${names[0] ?? ""}, +${names.length - 1}`, width, CUT);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function halfBlocks(pixels: readonly string[]): string[] {
|
|
333
|
+
const rows: string[] = [];
|
|
334
|
+
for (let row = 0; row < pixels.length; row += 2) {
|
|
335
|
+
const top = pixels[row] ?? "";
|
|
336
|
+
const bottom = pixels[row + 1] ?? "";
|
|
337
|
+
let line = "";
|
|
338
|
+
for (let column = 0; column < Math.max(top.length, bottom.length); column++) {
|
|
339
|
+
const up = top[column] === "#";
|
|
340
|
+
const down = bottom[column] === "#";
|
|
341
|
+
line += up && down ? "█" : up ? "▀" : down ? "▄" : " ";
|
|
342
|
+
}
|
|
343
|
+
rows.push(line);
|
|
344
|
+
}
|
|
345
|
+
return rows;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function pad(text: string, width: number): string {
|
|
349
|
+
const cells = visibleWidth(text);
|
|
350
|
+
if (cells <= width) return text + " ".repeat(width - cells);
|
|
351
|
+
const clipped = truncateToWidth(text, width, CUT);
|
|
352
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function center(text: string, width: number): string {
|
|
356
|
+
const cells = visibleWidth(text);
|
|
357
|
+
const fits = cells <= width;
|
|
358
|
+
const clipped = fits ? text : truncateToWidth(text, width, "");
|
|
359
|
+
const room = Math.max(0, width - (fits ? cells : visibleWidth(clipped)));
|
|
360
|
+
const before = Math.floor(room / 2);
|
|
361
|
+
return `${" ".repeat(before)}${clipped}${" ".repeat(room - before)}`;
|
|
362
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Ctrl+R over past prompts, newest first. It only knows the list; the editor does the keys. */
|
|
2
|
+
export interface HistorySearch {
|
|
3
|
+
query: string;
|
|
4
|
+
/** What the prompt held before, for when it's cancelled. */
|
|
5
|
+
draft: string;
|
|
6
|
+
entries: readonly string[];
|
|
7
|
+
at: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function startSearch(draft: string, entries: readonly string[]): HistorySearch {
|
|
11
|
+
return { query: "", draft, entries: [...new Set(entries)], at: -1 };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The next match from `from`, or -1. */
|
|
15
|
+
export function findPrompt(search: HistorySearch, from: number): number {
|
|
16
|
+
const wanted = search.query.toLowerCase();
|
|
17
|
+
return search.entries.findIndex((entry, index) => index >= from && entry.toLowerCase().includes(wanted));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Empty query: the prompt goes back to the draft. No match: it keeps showing the last one. */
|
|
21
|
+
export function shownText(search: HistorySearch): string {
|
|
22
|
+
return search.at < 0 ? search.draft : (search.entries[search.at] as string);
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Ctrl+C: stops the run if there is one, then asks once more before leaving.
|
|
2
|
+
// Pi's own double press has half a second, too short to read a message about it.
|
|
3
|
+
export const EXIT_WINDOW_MS = 2000;
|
|
4
|
+
|
|
5
|
+
export interface InterruptState {
|
|
6
|
+
/** The second press before this time exits. */
|
|
7
|
+
armedUntil?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type InterruptAction = "abort" | "arm" | "exit";
|
|
11
|
+
|
|
12
|
+
/** Stopping a run never arms the exit, so a double press to stop it cannot close Pi. */
|
|
13
|
+
export function nextInterrupt(
|
|
14
|
+
state: InterruptState,
|
|
15
|
+
idle: boolean,
|
|
16
|
+
now: number,
|
|
17
|
+
): { action: InterruptAction; state: InterruptState } {
|
|
18
|
+
if (!idle) return { action: "abort", state: {} };
|
|
19
|
+
if (state.armedUntil !== undefined && now < state.armedUntil) return { action: "exit", state: {} };
|
|
20
|
+
return { action: "arm", state: { armedUntil: now + EXIT_WINDOW_MS } };
|
|
21
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Alt+O: one tool call's whole output in a scrollable frame, newest first; the arrows move between calls.
|
|
2
|
+
import { callTarget, CUT, frame, frameInner, FRAME_Y, GLYPH, type KeyHint } from "../kit/index.ts";
|
|
3
|
+
import type { SessionEntry, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import {
|
|
5
|
+
type Component,
|
|
6
|
+
Key,
|
|
7
|
+
matchesKey,
|
|
8
|
+
truncateToWidth,
|
|
9
|
+
wrapTextWithAnsi,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
|
+
|
|
12
|
+
import { tabs } from "../render/format.ts";
|
|
13
|
+
import { plainText } from "../render/safe.ts";
|
|
14
|
+
|
|
15
|
+
export interface Output {
|
|
16
|
+
tool: string;
|
|
17
|
+
/** The path, the command, the query. */
|
|
18
|
+
target: string;
|
|
19
|
+
lines: string[];
|
|
20
|
+
failed: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function outputsOf(branch: readonly SessionEntry[]): Output[] {
|
|
24
|
+
const calls = new Map<string, { tool: string; target: string }>();
|
|
25
|
+
const outputs: Output[] = [];
|
|
26
|
+
for (const entry of branch) {
|
|
27
|
+
if (entry.type !== "message") continue;
|
|
28
|
+
const message = entry.message;
|
|
29
|
+
if (message.role === "assistant") {
|
|
30
|
+
for (const part of message.content)
|
|
31
|
+
if (part.type === "toolCall")
|
|
32
|
+
calls.set(part.id, { tool: part.name, target: callTarget(part.arguments) });
|
|
33
|
+
}
|
|
34
|
+
if (message.role !== "toolResult") continue;
|
|
35
|
+
const call = calls.get(message.toolCallId);
|
|
36
|
+
const printed = message.content
|
|
37
|
+
.map((part) => (part.type === "text" ? part.text : "[image]"))
|
|
38
|
+
.join("\n");
|
|
39
|
+
const text = tabs(plainText(printed)).trimEnd();
|
|
40
|
+
if (text === "") continue;
|
|
41
|
+
outputs.push({
|
|
42
|
+
tool: plainText(call?.tool ?? message.toolName),
|
|
43
|
+
target: plainText(call?.target ?? ""),
|
|
44
|
+
lines: text.split("\n"),
|
|
45
|
+
failed: message.isError,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return outputs;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface OutputViewOptions {
|
|
52
|
+
theme: Theme;
|
|
53
|
+
outputs: readonly Output[];
|
|
54
|
+
rows: () => number;
|
|
55
|
+
close: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class OutputView implements Component {
|
|
59
|
+
readonly #options: OutputViewOptions;
|
|
60
|
+
/** Counted from the newest. */
|
|
61
|
+
#index = 0;
|
|
62
|
+
#scroll = 0;
|
|
63
|
+
#width = 80;
|
|
64
|
+
/** Wrapped again only when it or the width changes. */
|
|
65
|
+
#wrapped: { output: Output; inner: number; lines: string[] } | undefined;
|
|
66
|
+
/** Drawn again only when a key moved something. */
|
|
67
|
+
#drawn:
|
|
68
|
+
| { output: Output; width: number; scroll: number; page: number; lines: string[] }
|
|
69
|
+
| undefined;
|
|
70
|
+
|
|
71
|
+
constructor(options: OutputViewOptions) {
|
|
72
|
+
this.#options = options;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
handleInput(data: string): void {
|
|
76
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.alt("o")) || data === "q")
|
|
77
|
+
return this.#options.close();
|
|
78
|
+
if (matchesKey(data, Key.up)) return this.#scrollBy(-1);
|
|
79
|
+
if (matchesKey(data, Key.down)) return this.#scrollBy(1);
|
|
80
|
+
if (matchesKey(data, Key.pageUp)) return this.#scrollBy(-this.#page());
|
|
81
|
+
if (matchesKey(data, Key.pageDown)) return this.#scrollBy(this.#page());
|
|
82
|
+
if (matchesKey(data, Key.home)) return this.#scrollBy(-Number.MAX_SAFE_INTEGER);
|
|
83
|
+
if (matchesKey(data, Key.end)) return this.#scrollBy(Number.MAX_SAFE_INTEGER);
|
|
84
|
+
if (matchesKey(data, Key.left)) return this.#move(1);
|
|
85
|
+
if (matchesKey(data, Key.right)) return this.#move(-1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
render(width: number): string[] {
|
|
89
|
+
this.#width = width;
|
|
90
|
+
const { theme, outputs } = this.#options;
|
|
91
|
+
const output = this.#current();
|
|
92
|
+
if (output === undefined)
|
|
93
|
+
return frame(theme, width, [theme.fg("muted", "No tool output in this session yet.")], {
|
|
94
|
+
title: "Tool output",
|
|
95
|
+
keys: [["esc", "close"]],
|
|
96
|
+
});
|
|
97
|
+
const lines = this.#lines(output);
|
|
98
|
+
const page = this.#page();
|
|
99
|
+
this.#scroll = Math.min(this.#scroll, Math.max(0, lines.length - page));
|
|
100
|
+
const known = this.#drawn;
|
|
101
|
+
if (
|
|
102
|
+
known !== undefined &&
|
|
103
|
+
known.output === output &&
|
|
104
|
+
known.width === width &&
|
|
105
|
+
known.scroll === this.#scroll &&
|
|
106
|
+
known.page === page
|
|
107
|
+
)
|
|
108
|
+
return known.lines;
|
|
109
|
+
|
|
110
|
+
const shown = lines
|
|
111
|
+
.slice(this.#scroll, this.#scroll + page)
|
|
112
|
+
.map((line) => theme.fg("toolOutput", line));
|
|
113
|
+
const last = Math.min(lines.length, this.#scroll + page);
|
|
114
|
+
const keys: KeyHint[] = [
|
|
115
|
+
["↑↓", "scroll"],
|
|
116
|
+
["←→", "calls"],
|
|
117
|
+
["esc", "close"],
|
|
118
|
+
];
|
|
119
|
+
const drawn = frame(theme, width, shown, {
|
|
120
|
+
title: this.#title(output),
|
|
121
|
+
status: theme.fg("muted", `${this.#index + 1} of ${outputs.length}`),
|
|
122
|
+
keys,
|
|
123
|
+
footnote: theme.fg("muted", `lines ${this.#scroll + 1}-${last} of ${lines.length}`),
|
|
124
|
+
});
|
|
125
|
+
this.#drawn = { output, width, scroll: this.#scroll, page, lines: drawn };
|
|
126
|
+
return drawn;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
invalidate(): void {
|
|
130
|
+
this.#wrapped = undefined;
|
|
131
|
+
this.#drawn = undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#title(output: Output): string {
|
|
135
|
+
const { theme } = this.#options;
|
|
136
|
+
const mark = output.failed ? theme.fg("error", GLYPH.cross) : theme.fg("success", GLYPH.check);
|
|
137
|
+
const room = Math.max(8, Math.floor(this.#width / 2) - output.tool.length);
|
|
138
|
+
const target = truncateToWidth(output.target, room, CUT);
|
|
139
|
+
return `${mark} ${theme.fg("toolTitle", theme.bold(output.tool))} ${theme.fg("muted", target)}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#current(): Output | undefined {
|
|
143
|
+
const { outputs } = this.#options;
|
|
144
|
+
return outputs[outputs.length - 1 - this.#index];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#lines(output: Output): string[] {
|
|
148
|
+
const inner = frameInner(this.#width);
|
|
149
|
+
const known = this.#wrapped;
|
|
150
|
+
if (known !== undefined && known.output === output && known.inner === inner) return known.lines;
|
|
151
|
+
const lines = output.lines.flatMap((line) => wrapTextWithAnsi(line, inner));
|
|
152
|
+
this.#wrapped = { output, inner, lines };
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#page(): number {
|
|
157
|
+
return Math.max(1, this.#options.rows() - FRAME_Y);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#scrollBy(rows: number): void {
|
|
161
|
+
this.#scroll = Math.max(0, this.#scroll + rows);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#move(step: number): void {
|
|
165
|
+
const next = this.#index + step;
|
|
166
|
+
if (next < 0 || next >= this.#options.outputs.length) return;
|
|
167
|
+
this.#index = next;
|
|
168
|
+
this.#scroll = 0;
|
|
169
|
+
}
|
|
170
|
+
}
|