@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.
Files changed (274) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -2
  4. package/SECURITY.md +5 -0
  5. package/package.json +112 -4
  6. package/packages/board/CHANGELOG.md +340 -0
  7. package/packages/board/CONTRIBUTING.md +7 -0
  8. package/packages/board/LICENSE +21 -0
  9. package/packages/board/README.md +88 -0
  10. package/packages/board/SECURITY.md +19 -0
  11. package/packages/board/docs/how-it-works.md +31 -0
  12. package/packages/board/docs/reference.md +278 -0
  13. package/packages/board/package.json +81 -0
  14. package/packages/board/runner/job-runner-legacy.mjs +68 -0
  15. package/packages/board/runner/job-runner.mjs +834 -0
  16. package/packages/board/runner/pty-runner-legacy.mjs +50 -0
  17. package/packages/board/runner/pty-runner.mjs +2175 -0
  18. package/packages/board/runner/state-coordinator.mjs +673 -0
  19. package/packages/board/runner/state-runner.mjs +203 -0
  20. package/packages/board/runner/title-runner.mjs +80 -0
  21. package/packages/board/src/commands/agent-board.ts +394 -0
  22. package/packages/board/src/commands/attach-decision.mjs +66 -0
  23. package/packages/board/src/commands/attach-flow.ts +251 -0
  24. package/packages/board/src/commands/bg.ts +103 -0
  25. package/packages/board/src/commands/board-model.ts +21 -0
  26. package/packages/board/src/core/atomic.mjs +184 -0
  27. package/packages/board/src/core/attach-delete.mjs +24 -0
  28. package/packages/board/src/core/attach-frame-proof.mjs +870 -0
  29. package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
  30. package/packages/board/src/core/attach-frame-stream.mjs +266 -0
  31. package/packages/board/src/core/attach-view-proof.mjs +709 -0
  32. package/packages/board/src/core/auto-state.mjs +377 -0
  33. package/packages/board/src/core/background-runs.mjs +273 -0
  34. package/packages/board/src/core/code-refs-store.mjs +315 -0
  35. package/packages/board/src/core/code-refs.mjs +913 -0
  36. package/packages/board/src/core/config.mjs +77 -0
  37. package/packages/board/src/core/control-protocol.mjs +482 -0
  38. package/packages/board/src/core/coordinator-client.mjs +633 -0
  39. package/packages/board/src/core/coordinator-journal.mjs +282 -0
  40. package/packages/board/src/core/coordinator-protocol.mjs +14 -0
  41. package/packages/board/src/core/cwd-stats.mjs +114 -0
  42. package/packages/board/src/core/dashboard-render.mjs +10 -0
  43. package/packages/board/src/core/derive.mjs +117 -0
  44. package/packages/board/src/core/diagnostics.mjs +133 -0
  45. package/packages/board/src/core/double-press.mjs +26 -0
  46. package/packages/board/src/core/editor-state-reporter.mjs +112 -0
  47. package/packages/board/src/core/events.mjs +286 -0
  48. package/packages/board/src/core/evidence.mjs +245 -0
  49. package/packages/board/src/core/follow-up-queue.mjs +211 -0
  50. package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
  51. package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
  52. package/packages/board/src/core/frame-witness-update.mjs +35 -0
  53. package/packages/board/src/core/heuristics.mjs +313 -0
  54. package/packages/board/src/core/hold-input.mjs +27 -0
  55. package/packages/board/src/core/host-coordination.mjs +300 -0
  56. package/packages/board/src/core/host-crash.mjs +79 -0
  57. package/packages/board/src/core/host-probe.mjs +207 -0
  58. package/packages/board/src/core/host-protocol.mjs +73 -0
  59. package/packages/board/src/core/ids.mjs +35 -0
  60. package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
  61. package/packages/board/src/core/invocation.mjs +43 -0
  62. package/packages/board/src/core/launch-options.mjs +418 -0
  63. package/packages/board/src/core/launch.mjs +157 -0
  64. package/packages/board/src/core/load-config.mjs +4 -0
  65. package/packages/board/src/core/locks.mjs +530 -0
  66. package/packages/board/src/core/paths.mjs +183 -0
  67. package/packages/board/src/core/pid.mjs +75 -0
  68. package/packages/board/src/core/prewarm-schedule.mjs +41 -0
  69. package/packages/board/src/core/process-identity-probe.mjs +228 -0
  70. package/packages/board/src/core/prompt-transport.mjs +13 -0
  71. package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
  72. package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
  73. package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
  74. package/packages/board/src/core/pty-attach-render.mjs +229 -0
  75. package/packages/board/src/core/pty-links.mjs +81 -0
  76. package/packages/board/src/core/pty-scroll.mjs +522 -0
  77. package/packages/board/src/core/pty-support.mjs +327 -0
  78. package/packages/board/src/core/repo.mjs +103 -0
  79. package/packages/board/src/core/rows.mjs +414 -0
  80. package/packages/board/src/core/screen-log-gc.mjs +198 -0
  81. package/packages/board/src/core/screen-log.mjs +160 -0
  82. package/packages/board/src/core/session-view.mjs +174 -0
  83. package/packages/board/src/core/slash-commands.mjs +52 -0
  84. package/packages/board/src/core/state-commands.mjs +757 -0
  85. package/packages/board/src/core/status-consistency.mjs +98 -0
  86. package/packages/board/src/core/steering-prompts.mjs +34 -0
  87. package/packages/board/src/core/steering.mjs +133 -0
  88. package/packages/board/src/core/stop-latch.mjs +41 -0
  89. package/packages/board/src/core/store.mjs +523 -0
  90. package/packages/board/src/core/suspend.mjs +36 -0
  91. package/packages/board/src/core/terminal-attach-client.mjs +981 -0
  92. package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
  93. package/packages/board/src/core/terminal-model.mjs +222 -0
  94. package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
  95. package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
  96. package/packages/board/src/core/terminal-snapshot.mjs +440 -0
  97. package/packages/board/src/core/test-knobs.mjs +28 -0
  98. package/packages/board/src/core/title.mjs +43 -0
  99. package/packages/board/src/core/types.mjs +414 -0
  100. package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
  101. package/packages/board/src/core/worktree.mjs +65 -0
  102. package/packages/board/src/index.ts +303 -0
  103. package/packages/board/src/runtime/service.mjs +3222 -0
  104. package/packages/board/src/ui/chrome.ts +20 -0
  105. package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
  106. package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
  107. package/packages/board/src/ui/dashboard.ts +2237 -0
  108. package/packages/board/src/ui/double-left.ts +30 -0
  109. package/packages/board/src/ui/external-editor.ts +25 -0
  110. package/packages/board/src/ui/focus.ts +32 -0
  111. package/packages/board/src/ui/pty-attach.ts +3184 -0
  112. package/packages/board/src/ui/waiting.ts +30 -0
  113. package/packages/commands/LICENSE +21 -0
  114. package/packages/commands/README.md +19 -0
  115. package/packages/commands/package.json +22 -0
  116. package/packages/commands/src/index.ts +43 -0
  117. package/packages/commands/src/security-review.ts +109 -0
  118. package/packages/frame/CHANGELOG.md +113 -0
  119. package/packages/frame/CONTRIBUTING.md +7 -0
  120. package/packages/frame/LICENSE +22 -0
  121. package/packages/frame/README.md +105 -0
  122. package/packages/frame/SECURITY.md +16 -0
  123. package/packages/frame/docs/how-it-works.md +53 -0
  124. package/packages/frame/docs/reference.md +78 -0
  125. package/packages/frame/package.json +72 -0
  126. package/packages/frame/src/data/git.ts +90 -0
  127. package/packages/frame/src/data/github.ts +140 -0
  128. package/packages/frame/src/data/gitlab.ts +63 -0
  129. package/packages/frame/src/data/mode.ts +49 -0
  130. package/packages/frame/src/data/past-prompts.ts +57 -0
  131. package/packages/frame/src/data/runner.ts +32 -0
  132. package/packages/frame/src/data/snapshot.ts +223 -0
  133. package/packages/frame/src/data/start-notes.ts +104 -0
  134. package/packages/frame/src/data/telemetry.ts +337 -0
  135. package/packages/frame/src/data/todos.ts +162 -0
  136. package/packages/frame/src/data/totals.ts +94 -0
  137. package/packages/frame/src/data/visits.ts +45 -0
  138. package/packages/frame/src/index.ts +696 -0
  139. package/packages/frame/src/kit/LICENSE +21 -0
  140. package/packages/frame/src/kit/app/attribution.ts +30 -0
  141. package/packages/frame/src/kit/app/builder.ts +186 -0
  142. package/packages/frame/src/kit/app/claim.ts +27 -0
  143. package/packages/frame/src/kit/app/feature.ts +91 -0
  144. package/packages/frame/src/kit/app/host.ts +214 -0
  145. package/packages/frame/src/kit/app/renderers.ts +62 -0
  146. package/packages/frame/src/kit/app/scope.ts +125 -0
  147. package/packages/frame/src/kit/board/model.ts +337 -0
  148. package/packages/frame/src/kit/contract.ts +79 -0
  149. package/packages/frame/src/kit/contracts/accounts.ts +33 -0
  150. package/packages/frame/src/kit/contracts/ask.ts +120 -0
  151. package/packages/frame/src/kit/contracts/board.ts +6 -0
  152. package/packages/frame/src/kit/contracts/by.ts +22 -0
  153. package/packages/frame/src/kit/contracts/calls.ts +46 -0
  154. package/packages/frame/src/kit/contracts/events.ts +89 -0
  155. package/packages/frame/src/kit/contracts/mode.ts +26 -0
  156. package/packages/frame/src/kit/contracts/names.ts +12 -0
  157. package/packages/frame/src/kit/contracts/peers.ts +77 -0
  158. package/packages/frame/src/kit/contracts/prompts.ts +8 -0
  159. package/packages/frame/src/kit/contracts/screen.ts +75 -0
  160. package/packages/frame/src/kit/contracts/skills.ts +81 -0
  161. package/packages/frame/src/kit/contracts/sources.ts +81 -0
  162. package/packages/frame/src/kit/contracts/views.ts +42 -0
  163. package/packages/frame/src/kit/control.ts +59 -0
  164. package/packages/frame/src/kit/dark-theme.ts +41 -0
  165. package/packages/frame/src/kit/decode.ts +210 -0
  166. package/packages/frame/src/kit/events/log.ts +313 -0
  167. package/packages/frame/src/kit/format.ts +25 -0
  168. package/packages/frame/src/kit/host.ts +4 -0
  169. package/packages/frame/src/kit/index.ts +25 -0
  170. package/packages/frame/src/kit/legacy-tasks.ts +39 -0
  171. package/packages/frame/src/kit/overlay-images.ts +36 -0
  172. package/packages/frame/src/kit/paths.ts +16 -0
  173. package/packages/frame/src/kit/screen/client.ts +74 -0
  174. package/packages/frame/src/kit/screen/feature.ts +64 -0
  175. package/packages/frame/src/kit/screen/model.ts +182 -0
  176. package/packages/frame/src/kit/screen/view.ts +838 -0
  177. package/packages/frame/src/kit/settings/files.ts +90 -0
  178. package/packages/frame/src/kit/settings/setting.ts +57 -0
  179. package/packages/frame/src/kit/settings/store.ts +192 -0
  180. package/packages/frame/src/kit/sources/file.ts +129 -0
  181. package/packages/frame/src/kit/sources/follow.ts +155 -0
  182. package/packages/frame/src/kit/testing.ts +260 -0
  183. package/packages/frame/src/kit/totals.ts +72 -0
  184. package/packages/frame/src/kit/ui/entry.ts +75 -0
  185. package/packages/frame/src/kit/ui/frame.ts +212 -0
  186. package/packages/frame/src/kit/ui/glyphs.ts +132 -0
  187. package/packages/frame/src/kit/ui/scroll.ts +14 -0
  188. package/packages/frame/src/render/diff.ts +157 -0
  189. package/packages/frame/src/render/fit.ts +86 -0
  190. package/packages/frame/src/render/format.ts +108 -0
  191. package/packages/frame/src/render/frame.ts +106 -0
  192. package/packages/frame/src/render/glyphs.ts +189 -0
  193. package/packages/frame/src/render/keys.ts +43 -0
  194. package/packages/frame/src/render/lead.ts +37 -0
  195. package/packages/frame/src/render/paint.ts +135 -0
  196. package/packages/frame/src/render/safe.ts +19 -0
  197. package/packages/frame/src/render/segments.ts +735 -0
  198. package/packages/frame/src/render/streaming.ts +223 -0
  199. package/packages/frame/src/render/timing.ts +106 -0
  200. package/packages/frame/src/render/tools.ts +167 -0
  201. package/packages/frame/src/settings.ts +105 -0
  202. package/packages/frame/src/ui/editor.ts +272 -0
  203. package/packages/frame/src/ui/effort.ts +36 -0
  204. package/packages/frame/src/ui/footer.ts +165 -0
  205. package/packages/frame/src/ui/header.ts +362 -0
  206. package/packages/frame/src/ui/history-search.ts +23 -0
  207. package/packages/frame/src/ui/interrupt.ts +21 -0
  208. package/packages/frame/src/ui/outputs.ts +170 -0
  209. package/packages/frame/src/ui/pi-entries.ts +298 -0
  210. package/packages/frame/src/ui/previews.ts +72 -0
  211. package/packages/frame/src/ui/queue.ts +75 -0
  212. package/packages/frame/src/ui/screen.ts +59 -0
  213. package/packages/frame/src/ui/timings.ts +48 -0
  214. package/packages/frame/src/ui/todos.ts +205 -0
  215. package/packages/frame/src/ui/tool-frame.ts +595 -0
  216. package/packages/frame/src/ui/working.ts +25 -0
  217. package/packages/frame/src/view/state.ts +97 -0
  218. package/packages/frame/src/view/transcript.ts +175 -0
  219. package/packages/frame/src/view/view.ts +162 -0
  220. package/packages/memory/CONTRIBUTING.md +7 -0
  221. package/packages/memory/LICENSE +21 -0
  222. package/packages/memory/README.md +44 -0
  223. package/packages/memory/SECURITY.md +20 -0
  224. package/packages/memory/package.json +55 -0
  225. package/packages/memory/skills/review-memory/SKILL.md +48 -0
  226. package/packages/memory/src/index.ts +163 -0
  227. package/packages/memory/src/prompt.ts +25 -0
  228. package/packages/memory/src/review.ts +150 -0
  229. package/packages/memory/src/secrets.ts +22 -0
  230. package/packages/memory/src/store.ts +173 -0
  231. package/packages/modes/CHANGELOG.md +104 -0
  232. package/packages/modes/CONTRIBUTING.md +7 -0
  233. package/packages/modes/LICENSE +21 -0
  234. package/packages/modes/README.md +174 -0
  235. package/packages/modes/SECURITY.md +17 -0
  236. package/packages/modes/package.json +55 -0
  237. package/packages/modes/skills/review-permissions/SKILL.md +54 -0
  238. package/packages/modes/src/bash.ts +218 -0
  239. package/packages/modes/src/deny-off.ts +62 -0
  240. package/packages/modes/src/git-safety.ts +72 -0
  241. package/packages/modes/src/index.ts +338 -0
  242. package/packages/modes/src/interview.ts +206 -0
  243. package/packages/modes/src/judge-gate.ts +113 -0
  244. package/packages/modes/src/modes.ts +121 -0
  245. package/packages/modes/src/review/apply.mjs +156 -0
  246. package/packages/modes/src/review/collect.mjs +208 -0
  247. package/packages/modes/src/review/tools.ts +71 -0
  248. package/packages/modes/src/sandbox.ts +37 -0
  249. package/packages/router/CHANGELOG.md +8 -0
  250. package/packages/router/LICENSE +21 -0
  251. package/packages/router/README.md +90 -0
  252. package/packages/router/package.json +51 -0
  253. package/packages/router/src/config.ts +65 -0
  254. package/packages/router/src/index.ts +172 -0
  255. package/packages/router/src/judge.ts +51 -0
  256. package/packages/router/src/route.ts +67 -0
  257. package/packages/web/CHANGELOG.md +36 -0
  258. package/packages/web/LICENSE +21 -0
  259. package/packages/web/README.md +87 -0
  260. package/packages/web/SECURITY.md +13 -0
  261. package/packages/web/package.json +59 -0
  262. package/packages/web/src/config.ts +109 -0
  263. package/packages/web/src/dnsfilter.ts +97 -0
  264. package/packages/web/src/extract.ts +65 -0
  265. package/packages/web/src/index.ts +217 -0
  266. package/packages/web/src/markdown.ts +122 -0
  267. package/packages/web/src/net.ts +109 -0
  268. package/packages/web/src/reader.ts +73 -0
  269. package/packages/web/src/sanitize.ts +50 -0
  270. package/packages/web/src/search.ts +76 -0
  271. package/packages/web/src/secrets.ts +22 -0
  272. package/packages/web/src/validate.ts +186 -0
  273. package/src/index.ts +156 -0
  274. package/src/settings.ts +79 -0
@@ -0,0 +1,272 @@
1
+ import { skillQueryAt } from "../kit/index.ts";
2
+ import { CustomEditor, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
3
+ import {
4
+ matchesKey,
5
+ type EditorTheme,
6
+ type TUI,
7
+ type TuiMouseEvent,
8
+ type TuiMouseEventResult,
9
+ truncateToWidth,
10
+ visibleWidth,
11
+ } from "@earendil-works/pi-tui";
12
+
13
+ import type { Piece } from "../render/fit.ts";
14
+ import { BOXES, border } from "../render/frame.ts";
15
+ import { paintLead, promptLead } from "../render/lead.ts";
16
+ import { SETTINGS_KEY, SETTINGS_SHORTCUT_INPUT } from "../render/keys.ts";
17
+ import type { CursorStyle } from "../settings.ts";
18
+ import { findPrompt, type HistorySearch, shownText, startSearch } from "./history-search.ts";
19
+ import { type Screen, type SlotName, slotPieces } from "./screen.ts";
20
+
21
+ type WorkingIndicator = Parameters<CustomEditor["setWorkingStatusIndicator"]>[0];
22
+
23
+ // Pi calls the border hooks inside its render; these mark where this class draws its own lines.
24
+ const TOP = "\u0000look:top\u0000";
25
+ const BOTTOM = "\u0000look:bottom\u0000";
26
+ // oxlint-disable-next-line no-control-regex -- pi's software cursor is an escape sequence
27
+ const INVERTED_CELL = /\x1b\[7m([\s\S]*?)\x1b\[0m/;
28
+ const MIN_WIDTH = 24;
29
+ const SEARCH_KEY = "ctrl+r";
30
+ const LEAD_MIN = 10;
31
+
32
+ const CURSOR_SEQUENCES: Record<CursorStyle, string> = {
33
+ block: "\x1b[0 q",
34
+ bar: "\x1b[6 q",
35
+ underline: "\x1b[4 q",
36
+ };
37
+
38
+ export class FrameEditor extends CustomEditor {
39
+ readonly #screen: Screen;
40
+ readonly #keybindings: KeybindingsManager;
41
+ #indicator: WorkingIndicator;
42
+ #framing = false;
43
+ #hiddenAbove = 0;
44
+ #hiddenBelow = 0;
45
+ #inset = 0;
46
+ #search: HistorySearch | undefined;
47
+
48
+ constructor(
49
+ tui: TUI,
50
+ theme: EditorTheme,
51
+ keybindings: KeybindingsManager,
52
+ screen: Screen,
53
+ options: { autocompleteMaxVisible?: number } = {},
54
+ ) {
55
+ super(tui, theme, keybindings, { ...options, paddingX: 0, embedWorkingStatus: true });
56
+ this.#screen = screen;
57
+ this.#keybindings = keybindings;
58
+ }
59
+
60
+ /** The frame owns the horizontal inset, so pi's padding would double it. */
61
+ override setPaddingX(_padding: number): void {
62
+ super.setPaddingX(0);
63
+ }
64
+
65
+ override setWorkingStatusIndicator(indicator: WorkingIndicator): void {
66
+ super.setWorkingStatusIndicator(indicator);
67
+ this.#indicator = indicator;
68
+ this.tui.requestRender();
69
+ }
70
+
71
+ /**
72
+ * Pi opens and reopens its palette only at the start of a message. Further in, any edit that
73
+ * leaves the cursor in a `/name` (a typed slash, a deleted or retyped letter) lists the skills.
74
+ */
75
+ override handleInput(data: string): void {
76
+ if (this.#search) {
77
+ this.#searchKey(this.#search, data);
78
+ return;
79
+ }
80
+ if (matchesKey(data, SEARCH_KEY) && !this.isShowingAutocomplete()) {
81
+ // What Up browses, then what was typed in this folder's other sessions.
82
+ const own: unknown = Reflect.get(this, "history");
83
+ this.#search = startSearch(this.getText(), [
84
+ ...(Array.isArray(own) ? (own as string[]) : []),
85
+ ...this.#screen.pastPrompts(),
86
+ ]);
87
+ return;
88
+ }
89
+ if (this.#keybindings.matches(data, "app.clear") && !this.isShowingAutocomplete()) {
90
+ this.#screen.interrupt(() => this.setText(""));
91
+ return;
92
+ }
93
+ if (matchesKey(data, SETTINGS_KEY)) {
94
+ super.handleInput(SETTINGS_SHORTCUT_INPUT);
95
+ return;
96
+ }
97
+ const before = this.getText();
98
+ super.handleInput(data);
99
+ if (this.isShowingAutocomplete() || !this.#screen.skills() || this.getText() === before) return;
100
+ const { line, col } = this.getCursor();
101
+ if (skillQueryAt(this.getLines(), line, col) !== undefined) openAutocomplete(this);
102
+ }
103
+
104
+ /** Enter sends the match, Esc goes back, and any other key keeps the match to edit. */
105
+ #searchKey(search: HistorySearch, data: string): void {
106
+ if (matchesKey(data, SEARCH_KEY)) {
107
+ const next = findPrompt(search, search.at + 1);
108
+ if (next >= 0) search.at = next;
109
+ } else if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || matchesKey(data, "ctrl+g")) {
110
+ this.#search = undefined;
111
+ this.setText(search.draft);
112
+ return;
113
+ } else if (matchesKey(data, "backspace")) {
114
+ search.query = search.query.slice(0, -1);
115
+ search.at = search.query ? findPrompt(search, 0) : -1;
116
+ } else if (data.length > 0 && !data.startsWith("\x1b") && data >= " " && !matchesKey(data, "enter")) {
117
+ search.query += data;
118
+ search.at = findPrompt(search, 0);
119
+ } else {
120
+ this.#search = undefined;
121
+ this.handleInput(data);
122
+ return;
123
+ }
124
+ this.setText(shownText(search));
125
+ }
126
+
127
+ override handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
128
+ if (this.#inset === 0) return super.handleMouse(event);
129
+ return super.handleMouse({
130
+ ...event,
131
+ x: event.x - this.#inset,
132
+ width: Math.max(1, event.width - 2 * this.#inset),
133
+ });
134
+ }
135
+
136
+ protected override renderTopBorder(width: number, hiddenLineCount: number): string {
137
+ if (!this.#framing) return super.renderTopBorder(width, hiddenLineCount);
138
+ this.#hiddenAbove = hiddenLineCount;
139
+ return TOP;
140
+ }
141
+
142
+ protected override renderBottomBorder(width: number, hiddenLineCount: number): string {
143
+ if (!this.#framing) return super.renderBottomBorder(width, hiddenLineCount);
144
+ this.#hiddenBelow = hiddenLineCount;
145
+ return BOTTOM;
146
+ }
147
+
148
+ override render(width: number): string[] {
149
+ const style = this.#screen.frameStyle();
150
+ if (style === "off" || width < MIN_WIDTH) {
151
+ this.#inset = 0;
152
+ return super.render(width);
153
+ }
154
+
155
+ const box = BOXES[style];
156
+ const glyphs = this.#screen.glyphs();
157
+ this.#inset = box.vertical === "" ? 0 : 2;
158
+ const inner = Math.max(1, width - 2 * this.#inset);
159
+
160
+ const cursor = this.#screen.cursor();
161
+ // Pi restores its own cursor setting after a reload, which would hide a bar cursor.
162
+ if (cursor !== "block" && !this.tui.getShowHardwareCursor()) applyCursor(this.tui, cursor);
163
+
164
+ this.#framing = true;
165
+ let lines: string[];
166
+ try {
167
+ lines = super.render(inner);
168
+ } finally {
169
+ this.#framing = false;
170
+ }
171
+
172
+ const paint = this.#screen.paint(
173
+ this.#screen.frameColor() ?? ((text) => this.borderColor(text)),
174
+ );
175
+ const snapshot = this.#screen.snapshot();
176
+ const separator = paint.dim(this.#screen.separator());
177
+ const pieces = (name: SlotName): Piece[] => slotPieces(this.#screen, name, snapshot, paint);
178
+ const scroll = (arrow: string, hidden: number): Piece[] =>
179
+ hidden > 0 ? [{ text: paint.dim(`${arrow} ${hidden} more`), priority: 1000 }] : [];
180
+ const options = { box, paint: paint.frame, separator, ellipsis: glyphs.ellipsis };
181
+
182
+ const left = style === "line" ? "" : `${paint.frame(box.vertical)} `;
183
+ const right = style === "line" ? "" : ` ${paint.frame(box.vertical)}`;
184
+ const result: string[] = [];
185
+ let inside = false;
186
+ let first = this.#hiddenAbove === 0;
187
+ const text = this.getText();
188
+ const lead = promptLead(text);
189
+
190
+ for (const line of lines) {
191
+ if (line === TOP) {
192
+ const lead =
193
+ this.#indicator === undefined ? undefined : (budget: number) => this.#lead(budget);
194
+ result.push(
195
+ border(
196
+ width,
197
+ pieces("topLeft"),
198
+ [...scroll("↑", this.#hiddenAbove), ...pieces("topRight")],
199
+ {
200
+ ...options,
201
+ edge: "top",
202
+ lead,
203
+ leadMin: LEAD_MIN,
204
+ },
205
+ ),
206
+ );
207
+ inside = true;
208
+ } else if (line === BOTTOM) {
209
+ result.push(
210
+ border(
211
+ width,
212
+ pieces("bottomLeft"),
213
+ [...scroll("↓", this.#hiddenBelow), ...pieces("bottomRight")],
214
+ { ...options, edge: "bottom" },
215
+ ),
216
+ );
217
+ inside = false;
218
+ } else if (inside) {
219
+ let shown = this.#screen.decorate(cursor === "block" ? line : withoutSoftwareCursor(line));
220
+ if (first && lead) shown = paintLead(shown, lead.length, this.#screen.promptColor(lead.kind));
221
+ first = false;
222
+ result.push(`${left}${fill(shown, inner)}${right}`);
223
+ } else {
224
+ result.push(`${" ".repeat(this.#inset)}${line}`);
225
+ }
226
+ }
227
+
228
+ if (this.#search) {
229
+ const { query, at } = this.#search;
230
+ const state = query && at < 0 ? " (no match)" : "";
231
+ result.push(
232
+ ` ${paint.dim(`search history: ${query}${state} ctrl+r older · enter send · esc cancel`)}`,
233
+ );
234
+ }
235
+
236
+ // Every line is drawn to the width already, and measuring is cheaper than cutting.
237
+ return result.map((line) =>
238
+ visibleWidth(line) > width ? truncateToWidth(line, width, "") : line,
239
+ );
240
+ }
241
+
242
+ #lead(budget: number): string | undefined {
243
+ const indicator = this.#indicator;
244
+ if (indicator === undefined) return undefined;
245
+ const full = indicator.renderInBorder(budget);
246
+ if (visibleWidth(full) > 0 && visibleWidth(full) <= budget) return full;
247
+ return indicator.renderSpinnerInBorder(budget);
248
+ }
249
+ }
250
+
251
+ /** The method is private; nothing opens if a pi release renames it. */
252
+ function openAutocomplete(editor: CustomEditor): void {
253
+ const open: unknown = Reflect.get(editor, "tryTriggerAutocomplete");
254
+ if (typeof open === "function") open.call(editor);
255
+ }
256
+
257
+ /** With the terminal's cursor showing, pi's inverted cell would be a second cursor. */
258
+ function withoutSoftwareCursor(line: string): string {
259
+ return line.replace(INVERTED_CELL, "$1");
260
+ }
261
+
262
+ function fill(line: string, width: number): string {
263
+ const cells = visibleWidth(line);
264
+ if (cells <= width) return line + " ".repeat(width - cells);
265
+ const clipped = truncateToWidth(line, width, "");
266
+ return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
267
+ }
268
+
269
+ export function applyCursor(tui: TUI, style: CursorStyle): void {
270
+ if (style !== "block") tui.setShowHardwareCursor(true);
271
+ tui.terminal.write(CURSOR_SEQUENCES[style]);
272
+ }
@@ -0,0 +1,36 @@
1
+ // `/effort`: the thinking level by name, for when Shift+Tab belongs to another extension.
2
+ import type { FeatureScope } from "../kit/index.ts";
3
+
4
+ import { EFFORT_LEVELS } from "../render/paint.ts";
5
+
6
+ const EFFORTS = ["off", ...EFFORT_LEVELS] as const;
7
+ type Effort = (typeof EFFORTS)[number];
8
+
9
+ function isEffort(value: string): value is Effort {
10
+ return (EFFORTS as readonly string[]).includes(value);
11
+ }
12
+
13
+ export function registerEffort(scope: FeatureScope): void {
14
+ scope.registerCommand("effort", {
15
+ description: "Change how hard the model thinks",
16
+ getArgumentCompletions: (prefix) => {
17
+ const found = EFFORTS.filter((level) => level.startsWith(prefix.trim().toLowerCase()));
18
+ return found.length === 0 ? null : found.map((level) => ({ value: level, label: level }));
19
+ },
20
+ handler: async (args, ctx) => {
21
+ const asked =
22
+ args.trim().toLowerCase() ||
23
+ (await ctx.ui.select(`Effort (now ${scope.getThinkingLevel()})`, [...EFFORTS]));
24
+ if (asked === undefined) return;
25
+ if (!isEffort(asked)) {
26
+ ctx.ui.notify(`Unknown effort "${asked}". Use one of: ${EFFORTS.join(", ")}`, "error");
27
+ return;
28
+ }
29
+ scope.setThinkingLevel(asked);
30
+ // Pi clamps it to what the model can do.
31
+ const level = scope.getThinkingLevel();
32
+ if (level === asked) ctx.ui.notify(`Effort: ${level}`, "info");
33
+ else ctx.ui.notify(`This model can't use ${asked}, so effort is ${level}`, "warning");
34
+ },
35
+ });
36
+ }
@@ -0,0 +1,165 @@
1
+ import type { ReadonlyFooterDataProvider } from "@earendil-works/pi-coding-agent";
2
+ import { type Component, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+
4
+ import { fitLine, fitRegions, type Piece } from "../render/fit.ts";
5
+ import type { Paint } from "../render/paint.ts";
6
+ import { type Screen, type SlotName, slotPieces } from "./screen.ts";
7
+
8
+ const GAP = 3;
9
+ /** So the strip's changing width never moves the right side. */
10
+ const STRIP_ROOM = 56;
11
+ /** A keystroke always redraws; nothing shown here reads differently 100 ms later. */
12
+ const REDRAW_MS = 100;
13
+
14
+ /** Kept per width and screen revision, since the transcript redraws on every keystroke. */
15
+ class KeptDrawing {
16
+ #drawn: { width: number; revision: number; at: number; statuses: string; lines: string[] } | undefined;
17
+
18
+ constructor(
19
+ private readonly screen: Screen,
20
+ private readonly draw: (width: number) => string[],
21
+ ) {}
22
+
23
+ render(width: number): string[] {
24
+ const now = Date.now();
25
+ const revision = this.screen.revision();
26
+ const drawn = this.#drawn;
27
+ // A status set as a dialog closes comes right after pi's own redraw.
28
+ const statuses = this.screen.statusesKey();
29
+ if (
30
+ drawn !== undefined &&
31
+ drawn.width === width &&
32
+ drawn.revision === revision &&
33
+ drawn.statuses === statuses &&
34
+ now - drawn.at < REDRAW_MS
35
+ )
36
+ return drawn.lines;
37
+ const lines = this.draw(width);
38
+ this.#drawn = { width, revision, at: now, statuses, lines };
39
+ return lines;
40
+ }
41
+
42
+ invalidate(): void {
43
+ this.#drawn = undefined;
44
+ }
45
+ }
46
+
47
+ function indentOf(screen: Screen): number {
48
+ const style = screen.frameStyle();
49
+ return style === "off" || style === "line" ? 1 : 2;
50
+ }
51
+
52
+ export class StripComponent implements Component {
53
+ readonly #screen: Screen;
54
+ readonly #kept: KeptDrawing;
55
+
56
+ constructor(screen: Screen) {
57
+ this.#screen = screen;
58
+ this.#kept = new KeptDrawing(screen, (width) => this.#draw(width));
59
+ }
60
+
61
+ render(width: number): string[] {
62
+ return this.#kept.render(width);
63
+ }
64
+
65
+ invalidate(): void {
66
+ this.#kept.invalidate();
67
+ }
68
+
69
+ #draw(width: number): string[] {
70
+ const screen = this.#screen;
71
+ const indent = indentOf(screen);
72
+ const room = width - indent * 2;
73
+ const pad = (line: string) => (line === "" ? "" : `${" ".repeat(indent)}${line}`);
74
+ const right = slotPieces(screen, "stripRight", screen.snapshot(), screen.paint());
75
+ // Always one line, so the editor does not jump when the first answer arrives.
76
+ if (right.length === 0 || room <= 0) return [pad(renderSlot(screen, "strip", room))];
77
+
78
+ const full = fitLine(right, Number.MAX_SAFE_INTEGER, fitOptions(screen, screen.paint()));
79
+ const beside = room - visibleWidth(full) - GAP;
80
+ // Beside the strip, the right side stays whole and the strip gives way.
81
+ if (beside >= STRIP_ROOM) {
82
+ const left = renderSlot(screen, "strip", beside);
83
+ const gap = " ".repeat(Math.max(GAP, room - visibleWidth(left) - visibleWidth(full)));
84
+ return [pad(`${left}${gap}${full}`)];
85
+ }
86
+ return [pad(renderSlot(screen, "strip", room)), pad(renderSlot(screen, "stripRight", room))];
87
+ }
88
+ }
89
+
90
+ export interface FooterHooks {
91
+ attach(data: ReadonlyFooterDataProvider, repaint: () => void): void;
92
+ detach(): void;
93
+ }
94
+
95
+ export class FooterComponent implements Component {
96
+ readonly #screen: Screen;
97
+ readonly #hooks: FooterHooks;
98
+ readonly #stopWatching: () => void;
99
+ readonly #kept: KeptDrawing;
100
+
101
+ constructor(tui: TUI, data: ReadonlyFooterDataProvider, screen: Screen, hooks: FooterHooks) {
102
+ this.#screen = screen;
103
+ this.#hooks = hooks;
104
+ this.#kept = new KeptDrawing(screen, (width) => this.#draw(width));
105
+ hooks.attach(data, () => tui.requestRender());
106
+ this.#stopWatching = data.onBranchChange(() => {
107
+ // A new branch is drawn now, not on the next keystroke.
108
+ screen.bump();
109
+ tui.requestRender();
110
+ });
111
+ }
112
+
113
+ render(width: number): string[] {
114
+ return this.#kept.render(width);
115
+ }
116
+
117
+ invalidate(): void {
118
+ this.#kept.invalidate();
119
+ }
120
+
121
+ #draw(width: number): string[] {
122
+ const screen = this.#screen;
123
+ const indent = indentOf(screen);
124
+ const room = width - indent * 2;
125
+ const pad = " ".repeat(indent);
126
+ const lines: string[] = [];
127
+
128
+ if (screen.frameStyle() === "off") {
129
+ lines.push(spread(screen, "topLeft", "topRight", room));
130
+ lines.push(spread(screen, "bottomLeft", "bottomRight", room));
131
+ }
132
+ const notice = screen.notice();
133
+ lines.push(notice === undefined ? spread(screen, "below", "belowRight", room) : truncateToWidth(notice, room));
134
+
135
+ const kept = lines.filter((line) => line !== "").map((line) => `${pad}${line}`);
136
+ return kept.length === 0 ? [""] : kept;
137
+ }
138
+
139
+ dispose(): void {
140
+ this.#stopWatching();
141
+ this.#hooks.detach();
142
+ }
143
+ }
144
+
145
+ function renderSlot(screen: Screen, name: SlotName, width: number): string {
146
+ if (width <= 0) return "";
147
+ const paint = screen.paint();
148
+ const pieces = slotPieces(screen, name, screen.snapshot(), paint);
149
+ return fitLine(pieces, width, fitOptions(screen, paint));
150
+ }
151
+
152
+ function spread(screen: Screen, leftSlot: SlotName, rightSlot: SlotName, width: number): string {
153
+ if (width <= 0) return "";
154
+ const paint = screen.paint();
155
+ const snapshot = screen.snapshot();
156
+ const left: Piece[] = slotPieces(screen, leftSlot, snapshot, paint);
157
+ const right: Piece[] = slotPieces(screen, rightSlot, snapshot, paint);
158
+ const [l = "", r = ""] = fitRegions([left, right], width - GAP, fitOptions(screen, paint));
159
+ if (r === "") return l;
160
+ return `${l}${" ".repeat(Math.max(GAP, width - visibleWidth(l) - visibleWidth(r)))}${r}`;
161
+ }
162
+
163
+ function fitOptions(screen: Screen, paint: Paint) {
164
+ return { separator: paint.dim(screen.separator()), ellipsis: screen.glyphs().ellipsis };
165
+ }