@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,78 @@
|
|
|
1
|
+
# Reference
|
|
2
|
+
|
|
3
|
+
Everything the [README](../README.md) leaves out: the colours, every setting and segment, and how a line fits.
|
|
4
|
+
|
|
5
|
+
## Colours
|
|
6
|
+
|
|
7
|
+
pi-frame never names a colour. Every piece names a **theme token**, so it follows whatever theme pi runs: `system`, `dark`, `light`, or a JSON of yours. Change the theme and every piece moves with it.
|
|
8
|
+
|
|
9
|
+
| What | Token |
|
|
10
|
+
| --------------------------------------------- | ----------------------------------------------------------- |
|
|
11
|
+
| Folder | `accent` |
|
|
12
|
+
| Branch | `syntaxFunction` |
|
|
13
|
+
| Ahead, staged / behind, modified / conflicted | `success` / `warning` / `error` |
|
|
14
|
+
| Machine, cache | `syntaxType` |
|
|
15
|
+
| Session name | `mdHeading` |
|
|
16
|
+
| Provider, model icon | `syntaxKeyword` |
|
|
17
|
+
| Model | `text`, bold |
|
|
18
|
+
| Sent `↑` | `mdLink` |
|
|
19
|
+
| Received `↓`, speeds, done | `success` |
|
|
20
|
+
| Time to first token, money | `syntaxNumber`, then `warning` past $1 and `error` past $10 |
|
|
21
|
+
| Context | `success`, `warning` past 70%, `error` past 90% |
|
|
22
|
+
| Effort level, the π | `thinkingMinimal` … `thinkingMax`, one per level |
|
|
23
|
+
| Labels, units, separators | `dim` |
|
|
24
|
+
| Editor frame | pi's effort border colour; with pi-modes, `warning` in auto, `accent` in plan, `success` in read-only |
|
|
25
|
+
|
|
26
|
+
Syntax tokens are borrowed on purpose: a theme spends its hues there, so they give the line variety that stays in the theme's family. The table lives in `src/render/paint.ts` as `ROLE_TOKENS`, and a test fails if any entry is ever a colour instead of a token.
|
|
27
|
+
|
|
28
|
+
## Settings
|
|
29
|
+
|
|
30
|
+
They live in the file `@adeildo/pi-kit` keeps, `~/.pi/agent/extensions/pi-harness/settings.json`, under `frame.`.
|
|
31
|
+
|
|
32
|
+
| Key | Default | Does |
|
|
33
|
+
| ------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `icons` | `"auto"` | `nerd`, `unicode` or `ascii`. Auto uses plain Unicode, or ASCII without UTF-8 |
|
|
35
|
+
| `style` | `"rounded"` | `rounded`, `square`, `heavy` or `line` (pi's two rules, written into) |
|
|
36
|
+
| `userFrame` | `true` | Your messages in a frame named `you`. Off, pi draws them its own way |
|
|
37
|
+
|
|
38
|
+
The rest is fixed. The line above the editor shows `elapsed` and `last`, with `board` on its right. The frame shows `branch` and `session` at the top left, `path` and `host` at the top right, the permission mode, `model` and `effort` at the bottom left, and `context` at the bottom right. The line below the editor shows `cost`, `tokens`, `cache`, `average` and `statuses`, with `quota` on its right. Every number has its word.
|
|
39
|
+
|
|
40
|
+
### Segments
|
|
41
|
+
|
|
42
|
+
What each piece of the screen shows.
|
|
43
|
+
|
|
44
|
+
| Id | Shows |
|
|
45
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| `path` | The working folder, with home as `~` |
|
|
47
|
+
| `branch` | The branch, ahead `↑` and behind `↓`, staged `+`, modified `~`, untracked `?`, conflicts `!`, stashes |
|
|
48
|
+
| `host` | The machine, so a pasted line says where it came from |
|
|
49
|
+
| `session` | The session name, once it has one |
|
|
50
|
+
| `board` | The card being done (`+1` when there are more), then how many open cards are blocked (`⚑ 1`, in yellow), are to do (`⧗ 3`) and are done (`✓ 2`, in green), from the session's board |
|
|
51
|
+
| `version` | The pi version |
|
|
52
|
+
| `clock` | The time |
|
|
53
|
+
| `model` | `provider/model`, with the account in parentheses when one is in use |
|
|
54
|
+
| `provider` | The provider on its own |
|
|
55
|
+
| `quota` | Every window of the plan, with its percent and its reset |
|
|
56
|
+
| `effort` | A divider and the effort level. Hidden for models that do not reason |
|
|
57
|
+
| `context` | Context used, the gauge, tokens over the window |
|
|
58
|
+
| `speed` | This answer's speeds, received `↓` and sent `↑` |
|
|
59
|
+
| `wait` | Time to the first token |
|
|
60
|
+
| `server` | The last call's time from the request leaving to the response headers |
|
|
61
|
+
| `elapsed` | The agent's working time, as a stopwatch, and its calls |
|
|
62
|
+
| `last` | The last finished call: its wait, its thought time and its writing speed |
|
|
63
|
+
| `request` | This answer's tokens, with the cache |
|
|
64
|
+
| `costRate` | This answer's cost per million tokens |
|
|
65
|
+
| `cost` | The session's cost |
|
|
66
|
+
| `tokens` | The session's tokens, with the cache |
|
|
67
|
+
| `cache` | The cache hit of the last prompt |
|
|
68
|
+
| `average` | The session's average speeds, both ways |
|
|
69
|
+
| `statuses` | The mode and sandbox state first, then what other packages report through `setStatus`, minus the ones placed on their own |
|
|
70
|
+
| `status:<key>` | One package's status, by the key it passes to `setStatus` |
|
|
71
|
+
|
|
72
|
+
A `status:<key>` segment is how a package gets a place of its own. [`@adeildo/pi-ask-permission`](../ask-permission) reports its mode as `pi-ask-permission:mode`, and the mode says what happens to what you type, so by default it sits in the frame next to the model instead of in the footer with everything else. `@adeildo/pi-providers` still reports its account as `pi-providers:account`, but the `model` segment draws it in parentheses, so the statuses segment skips it.
|
|
73
|
+
|
|
74
|
+
## How it fits
|
|
75
|
+
|
|
76
|
+
When a line runs out of room, the least important pieces take their short form first (the folder becomes its last part, the model loses its provider, the effort meter loses its label), and then leave, one at a time. The two sides of a border compete for the same width, so an unimportant piece on the right leaves before an important one on the left. A line never goes empty: the last piece stays and is cut.
|
|
77
|
+
|
|
78
|
+
The frame is the editor pi already has. Typing, history, autocomplete and every app key stay pi's; the class only changes what the lines look like and shifts mouse clicks by the width of the frame. The spinner is pi's, embedded through pi's own hook, and the working line only speaks through `setWorkingMessage`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huseynsnmz/pi-frame",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Pi's start screen, a framed editor, a box around every tool call and a session footer. A fork of @adeildo/pi-look.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi",
|
|
7
|
+
"pi-coding-agent",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi-package",
|
|
10
|
+
"statusline",
|
|
11
|
+
"theme",
|
|
12
|
+
"tui"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Hüseyin Sönmez",
|
|
17
|
+
"email": "huseyin@snmz.org",
|
|
18
|
+
"url": "https://github.com/huseynsnmz"
|
|
19
|
+
},
|
|
20
|
+
"contributors": [
|
|
21
|
+
{
|
|
22
|
+
"name": "adeildo",
|
|
23
|
+
"url": "https://github.com/felipeadeildo"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/huseynsnmz/pi-suite.git",
|
|
29
|
+
"directory": "packages/frame"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"docs",
|
|
34
|
+
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"type": "module",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": "./src/index.ts"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"check": "tsc --noEmit",
|
|
47
|
+
"test": "bun test",
|
|
48
|
+
"e2e": "python3 test/e2e/run.py"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@earendil-works/pi-ai": "*",
|
|
53
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
54
|
+
"@earendil-works/pi-tui": "*",
|
|
55
|
+
"typebox": "*"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@earendil-works/pi-ai": "1.5.0",
|
|
59
|
+
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
60
|
+
"@earendil-works/pi-tui": "1.5.0",
|
|
61
|
+
"@types/bun": "1.4.4",
|
|
62
|
+
"@types/node": "26.6.6",
|
|
63
|
+
"typebox": "1.4.3",
|
|
64
|
+
"typescript": "7.0.2"
|
|
65
|
+
},
|
|
66
|
+
"pi": {
|
|
67
|
+
"extensions": [
|
|
68
|
+
"./src/index.ts"
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"private": true
|
|
72
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { processRunner, type Runner } from "./runner.ts";
|
|
2
|
+
|
|
3
|
+
export interface GitState {
|
|
4
|
+
ahead: number;
|
|
5
|
+
behind: number;
|
|
6
|
+
staged: number;
|
|
7
|
+
modified: number;
|
|
8
|
+
untracked: number;
|
|
9
|
+
conflicted: number;
|
|
10
|
+
stashed: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function emptyGit(): GitState {
|
|
14
|
+
return { ahead: 0, behind: 0, staged: 0, modified: 0, untracked: 0, conflicted: 0, stashed: 0 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseStatus(output: string): GitState {
|
|
18
|
+
const state = emptyGit();
|
|
19
|
+
|
|
20
|
+
for (const line of output.split("\n")) {
|
|
21
|
+
if (line.startsWith("# branch.ab ")) {
|
|
22
|
+
const [, , ahead, behind] = line.split(" ");
|
|
23
|
+
state.ahead = Math.abs(Number(ahead ?? "0")) || 0;
|
|
24
|
+
state.behind = Math.abs(Number(behind ?? "0")) || 0;
|
|
25
|
+
} else if (line.startsWith("# stash ")) {
|
|
26
|
+
state.stashed = Number(line.slice("# stash ".length)) || 0;
|
|
27
|
+
} else if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
28
|
+
// `XY` is the index column, then the tree column, with `.` for unchanged.
|
|
29
|
+
const index = line[2];
|
|
30
|
+
const tree = line[3];
|
|
31
|
+
if (index !== undefined && index !== ".") state.staged++;
|
|
32
|
+
if (tree !== undefined && tree !== ".") state.modified++;
|
|
33
|
+
} else if (line.startsWith("u ")) {
|
|
34
|
+
state.conflicted++;
|
|
35
|
+
} else if (line.startsWith("? ")) {
|
|
36
|
+
state.untracked++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `git status` runs after every tool call, outside any permission check, so nothing the repository
|
|
45
|
+
* configures may run through it: an agent can write `.git/config`, and a downloaded folder can bring one.
|
|
46
|
+
*/
|
|
47
|
+
const SAFE_STATUS = [
|
|
48
|
+
"-c",
|
|
49
|
+
"core.fsmonitor=false",
|
|
50
|
+
"-c",
|
|
51
|
+
"core.hooksPath=/dev/null",
|
|
52
|
+
"--no-optional-locks",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** Keys and scopes separated by NUL, as `git config --show-scope --name-only --list -z` prints them. */
|
|
56
|
+
export function hasRepoFilter(config: string): boolean {
|
|
57
|
+
const fields = config.split("\0");
|
|
58
|
+
for (let at = 0; at + 1 < fields.length; at += 2) {
|
|
59
|
+
const scope = fields[at];
|
|
60
|
+
if (scope !== "local" && scope !== "worktree") continue;
|
|
61
|
+
if (/^filter\..+\.(?:clean|smudge|process)$/is.test(fields[at + 1] ?? "")) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function readGit(
|
|
67
|
+
cwd: string,
|
|
68
|
+
run: Runner = processRunner(cwd, 2000),
|
|
69
|
+
): Promise<GitState | undefined> {
|
|
70
|
+
try {
|
|
71
|
+
// A filter's name is free, so no option can turn them all off.
|
|
72
|
+
const config = await run("git", ["-C", cwd, "config", "--show-scope", "--name-only", "--list", "-z"]);
|
|
73
|
+
if (hasRepoFilter(config)) return undefined;
|
|
74
|
+
return parseStatus(
|
|
75
|
+
await run("git", [
|
|
76
|
+
...SAFE_STATUS,
|
|
77
|
+
"-C",
|
|
78
|
+
cwd,
|
|
79
|
+
"status",
|
|
80
|
+
"--porcelain=v2",
|
|
81
|
+
"--branch",
|
|
82
|
+
"--show-stash",
|
|
83
|
+
"--ignore-submodules=all",
|
|
84
|
+
]),
|
|
85
|
+
);
|
|
86
|
+
} catch {
|
|
87
|
+
// Not a repository, or git did not answer: the card says nothing.
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// CI's last run on this branch and your open pull requests that wait on you, asked once per launch.
|
|
2
|
+
// Shows nothing without the forge's tool, network or a remote it serves, and starts only that tool.
|
|
3
|
+
import { isObject } from "../kit/index.ts";
|
|
4
|
+
|
|
5
|
+
import { gitlabHosts, glabMayStart, readGitlab } from "./gitlab.ts";
|
|
6
|
+
import { processRunner, type Runner } from "./runner.ts";
|
|
7
|
+
|
|
8
|
+
const FAILED = new Set(["failure", "startup_failure", "timed_out"]);
|
|
9
|
+
/** Enough to tell a new comment from an old one. */
|
|
10
|
+
const PR_FIELDS = "number,title,author,reviewDecision,comments";
|
|
11
|
+
/** github.com, or an enterprise host like github.acme.dev. */
|
|
12
|
+
const GITHUB_REMOTE = /(?:@|\/\/)(?:[\w-]+\.)*github\.[\w.-]+[:/]/i;
|
|
13
|
+
|
|
14
|
+
interface CiFailure {
|
|
15
|
+
branch: string;
|
|
16
|
+
sha: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface OpenPullRequest {
|
|
21
|
+
number: number;
|
|
22
|
+
title: string;
|
|
23
|
+
waits: "changes requested" | "new comment" | "open threads";
|
|
24
|
+
/** How the forge names it. `PR #52` when unset. */
|
|
25
|
+
ref?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GithubState {
|
|
29
|
+
ci: CiFailure | undefined;
|
|
30
|
+
pr: OpenPullRequest | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Never rejects. `since` is the last session here, the bar for a comment to be news. */
|
|
34
|
+
export async function readGithub(
|
|
35
|
+
cwd: string,
|
|
36
|
+
since: number | undefined,
|
|
37
|
+
run: Runner = processRunner(cwd),
|
|
38
|
+
): Promise<GithubState> {
|
|
39
|
+
const remotes = (await ask(() => run("git", ["remote", "-v"]))) ?? "";
|
|
40
|
+
if (!GITHUB_REMOTE.test(remotes)) {
|
|
41
|
+
const hosts = gitlabHosts(remotes);
|
|
42
|
+
return glabMayStart(hosts) ? readGitlab(run, await branchOf(run)) : { ci: undefined, pr: undefined };
|
|
43
|
+
}
|
|
44
|
+
const [branch, pr] = await Promise.all([branchOf(run), pullRequest(run, since)]);
|
|
45
|
+
const ci = branch === undefined ? undefined : await failedRun(run, branch);
|
|
46
|
+
return { ci, pr };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function branchOf(run: Runner): Promise<string | undefined> {
|
|
50
|
+
const branch = (await ask(() => run("git", ["branch", "--show-current"])))?.trim();
|
|
51
|
+
return branch === undefined || branch === "" ? undefined : branch;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function failedRun(run: Runner, branch: string): Promise<CiFailure | undefined> {
|
|
55
|
+
const out = await ask(() =>
|
|
56
|
+
run("gh", [
|
|
57
|
+
"run",
|
|
58
|
+
"list",
|
|
59
|
+
"--branch",
|
|
60
|
+
branch,
|
|
61
|
+
"--limit",
|
|
62
|
+
"1",
|
|
63
|
+
"--json",
|
|
64
|
+
"conclusion,headSha,name,displayTitle",
|
|
65
|
+
]),
|
|
66
|
+
);
|
|
67
|
+
const [last] = rows(out);
|
|
68
|
+
if (last === undefined) return undefined;
|
|
69
|
+
const conclusion = field(last, "conclusion");
|
|
70
|
+
if (conclusion === undefined || !FAILED.has(conclusion)) return undefined;
|
|
71
|
+
return {
|
|
72
|
+
branch,
|
|
73
|
+
sha: (field(last, "headSha") ?? "").slice(0, 7),
|
|
74
|
+
name: field(last, "name") ?? field(last, "displayTitle") ?? "CI",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function pullRequest(
|
|
79
|
+
run: Runner,
|
|
80
|
+
since: number | undefined,
|
|
81
|
+
): Promise<OpenPullRequest | undefined> {
|
|
82
|
+
const out = await ask(() =>
|
|
83
|
+
run("gh", ["pr", "list", "--author", "@me", "--limit", "10", "--json", PR_FIELDS]),
|
|
84
|
+
);
|
|
85
|
+
for (const pr of rows(out)) {
|
|
86
|
+
const number = numberOf(pr);
|
|
87
|
+
const title = field(pr, "title");
|
|
88
|
+
if (number === undefined || title === undefined) continue;
|
|
89
|
+
if (field(pr, "reviewDecision") === "CHANGES_REQUESTED")
|
|
90
|
+
return { number, title, waits: "changes requested" };
|
|
91
|
+
if (hasNewComment(pr, since)) return { number, title, waits: "new comment" };
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasNewComment(pr: Record<string, unknown>, since: number | undefined): boolean {
|
|
97
|
+
if (since === undefined) return false;
|
|
98
|
+
const me = loginOf(pr["author"]);
|
|
99
|
+
const comments = pr["comments"];
|
|
100
|
+
if (!Array.isArray(comments)) return false;
|
|
101
|
+
return comments.some((comment) => {
|
|
102
|
+
if (!isObject(comment)) return false;
|
|
103
|
+
const at = Date.parse(field(comment, "createdAt") ?? "");
|
|
104
|
+
const login = loginOf(comment["author"]);
|
|
105
|
+
return Number.isFinite(at) && at > since && login !== undefined && login !== me;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function loginOf(value: unknown): string | undefined {
|
|
110
|
+
return isObject(value) ? field(value, "login") : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function ask(question: () => Promise<string>): Promise<string | undefined> {
|
|
114
|
+
try {
|
|
115
|
+
return await question();
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function rows(out: string | undefined): Record<string, unknown>[] {
|
|
122
|
+
if (out === undefined) return [];
|
|
123
|
+
let value: unknown;
|
|
124
|
+
try {
|
|
125
|
+
value = JSON.parse(out);
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
return Array.isArray(value) ? value.filter(isObject) : [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function field(row: Record<string, unknown>, key: string): string | undefined {
|
|
133
|
+
const value = row[key];
|
|
134
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function numberOf(pr: Record<string, unknown>, key = "number"): number | undefined {
|
|
138
|
+
const value = pr[key];
|
|
139
|
+
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
140
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The GitLab version of github.ts: the branch's last pipeline, and your merge requests that wait on you.
|
|
2
|
+
// glab sends a token from the environment to whichever host it is pointed at, and the repository
|
|
3
|
+
// chooses that host, so with such a token it starts only for gitlab.com or GITLAB_HOST.
|
|
4
|
+
import { ask, field, type GithubState, numberOf, rows } from "./github.ts";
|
|
5
|
+
import type { Runner } from "./runner.ts";
|
|
6
|
+
|
|
7
|
+
/** gitlab.com, or a self-hosted name like gitlab.acme.dev. */
|
|
8
|
+
const GITLAB_HOST = /(?:@|\/\/)((?:[\w-]+\.)*gitlab\.[\w.-]+)[:/]/gi;
|
|
9
|
+
const TOKENS = ["GITLAB_TOKEN", "GITLAB_ACCESS_TOKEN", "OAUTH_TOKEN"];
|
|
10
|
+
const WAITS = {
|
|
11
|
+
requested_changes: "changes requested",
|
|
12
|
+
discussions_not_resolved: "open threads",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export function gitlabHosts(remotes: string): string[] {
|
|
16
|
+
return [...remotes.matchAll(GITLAB_HOST)].map((match) => (match[1] ?? "").toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function glabMayStart(
|
|
20
|
+
hosts: readonly string[],
|
|
21
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
22
|
+
): boolean {
|
|
23
|
+
if (hosts.length === 0) return false;
|
|
24
|
+
if (!TOKENS.some((name) => env[name])) return true;
|
|
25
|
+
const trusted = ["gitlab.com", env["GITLAB_HOST"]?.replace(/^https?:\/\//, "").toLowerCase()];
|
|
26
|
+
return hosts.every((host) => trusted.includes(host));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Never rejects. */
|
|
30
|
+
export async function readGitlab(run: Runner, branch: string | undefined): Promise<GithubState> {
|
|
31
|
+
const [ci, pr] = await Promise.all([
|
|
32
|
+
branch === undefined ? undefined : failedPipeline(run, branch),
|
|
33
|
+
mergeRequest(run),
|
|
34
|
+
]);
|
|
35
|
+
return { ci, pr };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function failedPipeline(run: Runner, branch: string): Promise<GithubState["ci"]> {
|
|
39
|
+
const out = await ask(() =>
|
|
40
|
+
run("glab", ["ci", "list", "--ref", branch, "--per-page", "1", "--output", "json"]),
|
|
41
|
+
);
|
|
42
|
+
const [last] = rows(out);
|
|
43
|
+
if (last === undefined || field(last, "status") !== "failed") return undefined;
|
|
44
|
+
return {
|
|
45
|
+
branch,
|
|
46
|
+
sha: (field(last, "sha") ?? "").slice(0, 7),
|
|
47
|
+
name: field(last, "name") ?? "pipeline",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function mergeRequest(run: Runner): Promise<GithubState["pr"]> {
|
|
52
|
+
const out = await ask(() =>
|
|
53
|
+
run("glab", ["mr", "list", "--author", "@me", "--per-page", "10", "--output", "json"]),
|
|
54
|
+
);
|
|
55
|
+
for (const mr of rows(out)) {
|
|
56
|
+
const number = numberOf(mr, "iid");
|
|
57
|
+
const title = field(mr, "title");
|
|
58
|
+
const waits = WAITS[field(mr, "detailed_merge_status") as keyof typeof WAITS];
|
|
59
|
+
if (number !== undefined && title !== undefined && waits !== undefined)
|
|
60
|
+
return { number, title, waits, ref: `MR !${number}` };
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The mode pi-modes reports. It only publishes a status, so the text is the contract.
|
|
2
|
+
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
const MODE_STATUS = "pi-modes";
|
|
5
|
+
const REPORTED = /^mode: (normal|auto|plan|read-only|yolo)$/;
|
|
6
|
+
|
|
7
|
+
export type Mode = "normal" | "auto" | "plan" | "read-only" | "yolo";
|
|
8
|
+
|
|
9
|
+
/** The same colors pi-modes gives the word. In `normal`, the frame stays pi's own. */
|
|
10
|
+
const FRAME_COLORS = { auto: "warning", plan: "accent", "read-only": "success", yolo: "error" } as const;
|
|
11
|
+
|
|
12
|
+
export function modeOf(statuses: ReadonlyMap<string, string>): Mode | undefined {
|
|
13
|
+
const status = statuses.get(MODE_STATUS);
|
|
14
|
+
if (status === undefined) return undefined;
|
|
15
|
+
return REPORTED.exec(stripTerminalSequences(status).trim())?.[1] as Mode | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A message's frame stays pi's own in `normal` too. */
|
|
19
|
+
export function modeColor(mode: Mode): (typeof FRAME_COLORS)[keyof typeof FRAME_COLORS] | undefined {
|
|
20
|
+
return mode === "normal" ? undefined : FRAME_COLORS[mode];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function frameColorOf(
|
|
24
|
+
statuses: ReadonlyMap<string, string>,
|
|
25
|
+
): (typeof FRAME_COLORS)[keyof typeof FRAME_COLORS] | undefined {
|
|
26
|
+
const mode = modeOf(statuses);
|
|
27
|
+
return mode === undefined ? undefined : modeColor(mode);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Saved with the session, so a resumed conversation still shows which mode each message went out in. */
|
|
31
|
+
export const SENT_IN_ENTRY = "pi-frame-sent-in";
|
|
32
|
+
|
|
33
|
+
/** The session entry of each message sent in a mode, joined on the message's timestamp, since a live message has no entry yet. */
|
|
34
|
+
export function sentInFromEntries(
|
|
35
|
+
entries: readonly { type: string; id?: string; customType?: string; data?: unknown; message?: { role?: string; timestamp?: number } }[],
|
|
36
|
+
): Map<string, Mode> {
|
|
37
|
+
const byTime = new Map<number, Mode>();
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (entry.type !== "custom" || entry.customType !== SENT_IN_ENTRY) continue;
|
|
40
|
+
const { at, mode } = (entry.data ?? {}) as { at?: unknown; mode?: unknown };
|
|
41
|
+
if (typeof at === "number" && typeof mode === "string" && REPORTED.test(`mode: ${mode}`)) byTime.set(at, mode as Mode);
|
|
42
|
+
}
|
|
43
|
+
const sent = new Map<string, Mode>();
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const mode = entry.type === "message" && entry.message?.role === "user" ? byTime.get(entry.message.timestamp ?? -1) : undefined;
|
|
46
|
+
if (mode !== undefined && entry.id !== undefined) sent.set(entry.id, mode);
|
|
47
|
+
}
|
|
48
|
+
return sent;
|
|
49
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const NEWEST_FILES = 20;
|
|
5
|
+
const MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
6
|
+
const MAX_PROMPTS = 1000;
|
|
7
|
+
|
|
8
|
+
function promptOf(line: string): string | undefined {
|
|
9
|
+
if (!line.includes('"role":"user"')) return undefined;
|
|
10
|
+
try {
|
|
11
|
+
const entry = JSON.parse(line) as { type?: string; message?: { role?: string; content?: unknown } };
|
|
12
|
+
if (entry.type !== "message" || entry.message?.role !== "user") return undefined;
|
|
13
|
+
const content = entry.message.content;
|
|
14
|
+
if (typeof content === "string") return content.trim() || undefined;
|
|
15
|
+
if (!Array.isArray(content)) return undefined;
|
|
16
|
+
const text = content
|
|
17
|
+
.filter((part): part is { type: "text"; text: string } => part?.type === "text" && typeof part.text === "string")
|
|
18
|
+
.map((part) => part.text)
|
|
19
|
+
.join("\n")
|
|
20
|
+
.trim();
|
|
21
|
+
return text || undefined;
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What was typed in this folder's recent sessions, newest first. A file it can't read is skipped. */
|
|
28
|
+
export function pastPrompts(sessionDir: string): string[] {
|
|
29
|
+
const prompts: string[] = [];
|
|
30
|
+
let files: { path: string; modified: number; size: number }[] = [];
|
|
31
|
+
try {
|
|
32
|
+
files = readdirSync(sessionDir)
|
|
33
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
34
|
+
.map((name) => {
|
|
35
|
+
const path = join(sessionDir, name);
|
|
36
|
+
const { mtimeMs, size } = statSync(path);
|
|
37
|
+
return { path, modified: mtimeMs, size };
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
files.sort((a, b) => b.modified - a.modified);
|
|
43
|
+
for (const { path, size } of files.slice(0, NEWEST_FILES)) {
|
|
44
|
+
if (size > MAX_FILE_BYTES || prompts.length >= MAX_PROMPTS) continue;
|
|
45
|
+
try {
|
|
46
|
+
const found: string[] = [];
|
|
47
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
48
|
+
const prompt = promptOf(line);
|
|
49
|
+
if (prompt) found.push(prompt);
|
|
50
|
+
}
|
|
51
|
+
prompts.push(...found.reverse());
|
|
52
|
+
} catch {
|
|
53
|
+
/* it may be gone, or still being written */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return prompts.slice(0, MAX_PROMPTS);
|
|
57
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/** The start card is never worth a longer wait. */
|
|
4
|
+
const TIMEOUT_MS = 3000;
|
|
5
|
+
/** Room for a page of answers, like your open pull requests with their comments. */
|
|
6
|
+
const MAX_OUTPUT = 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
/** No download for a partial clone, whose transport the repository's config chooses, and no prompt. */
|
|
9
|
+
const QUIET_GIT = {
|
|
10
|
+
...process.env,
|
|
11
|
+
GIT_NO_LAZY_FETCH: "1",
|
|
12
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
13
|
+
GLAB_CHECK_UPDATE: "false",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Rejects when it fails or takes too long. */
|
|
17
|
+
export type Runner = (file: string, args: readonly string[]) => Promise<string>;
|
|
18
|
+
|
|
19
|
+
export function processRunner(cwd: string, timeoutMs: number = TIMEOUT_MS): Runner {
|
|
20
|
+
return (file, args) =>
|
|
21
|
+
new Promise((resolve, reject) => {
|
|
22
|
+
execFile(
|
|
23
|
+
file,
|
|
24
|
+
[...args],
|
|
25
|
+
{ cwd, timeout: timeoutMs, maxBuffer: MAX_OUTPUT, env: QUIET_GIT },
|
|
26
|
+
(error, stdout) => {
|
|
27
|
+
if (error === null) resolve(stdout);
|
|
28
|
+
else reject(error instanceof Error ? error : new Error(String(error)));
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|