agentbox-app 0.1.6
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/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- package/worker-permissions.json +6 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// 2026-09-16: generated headless commands plus names advertised by the current
|
|
2
|
+
// native session. The eight-command notes below describe the original design.
|
|
3
|
+
// WHAT "/" MEANS IN AGENTBOX, NOW THAT IT MEANS TWO THINGS.
|
|
4
|
+
//
|
|
5
|
+
// She was then given three ways to do it and picked the middle one: "Keep the
|
|
6
|
+
// six modes at the top and add only the ones that say something about how a
|
|
7
|
+
// message runs: /model, /effort, /fast, /goal, /context, /usage, /mcp,
|
|
8
|
+
// /compact. Read them out of the installed binary at build time rather than
|
|
9
|
+
// typing them into a file, so the list cannot rot quietly."
|
|
10
|
+
//
|
|
11
|
+
// THE READING HALF IS scripts/read-claude-commands.mjs AND THE TABLE IT WRITES.
|
|
12
|
+
// This file is the thinking on top of it, and it is short on purpose.
|
|
13
|
+
//
|
|
14
|
+
// THE ONE MEASUREMENT THAT SHAPED EVERYTHING HERE, taken on 2026-08-27 against
|
|
15
|
+
// the installed 2.1.246, because the obvious design does not work:
|
|
16
|
+
//
|
|
17
|
+
// claude -p '/context
|
|
18
|
+
//
|
|
19
|
+
// Then in one short sentence say what file is in this directory.'
|
|
20
|
+
//
|
|
21
|
+
// returned the context table and NOTHING ELSE. 142 lines, zero mentions of
|
|
22
|
+
// the file and zero mentions of the word directory. The command ate the
|
|
23
|
+
// message.
|
|
24
|
+
//
|
|
25
|
+
// SO A COMMAND CANNOT RIDE ALONG WITH HER REPLY. It is not a modifier on a
|
|
26
|
+
// message the way a permission mode is; it is a turn of its own, exactly as it
|
|
27
|
+
// is in Claude Code, where typing /context does not also send whatever you had
|
|
28
|
+
// half-written. That is why picking one of these eight from the menu TYPES it
|
|
29
|
+
// into the box instead of setting a hidden value on the send: what she is about
|
|
30
|
+
// to do is send a command, and the box should say so before she presses Send.
|
|
31
|
+
//
|
|
32
|
+
// AND WHAT COMES BACK IS THE CONFIRMATION SHE ASKED FOR. Agentbox's trace
|
|
33
|
+
// already renders assistant text into the thread, so the answer arrives on the
|
|
34
|
+
// row by itself. Nothing had to be invented to show it.
|
|
35
|
+
//
|
|
36
|
+
// THE SIX MODES ARE NOT IN HERE and must not move into it. They are ours, they
|
|
37
|
+
// set a value on a message she is still writing, and five of the six are not
|
|
38
|
+
// Claude Code commands at all. modes.ts says all of that at length. The menu
|
|
39
|
+
// draws both lists; they are two different verbs and the menu labels them so.
|
|
40
|
+
|
|
41
|
+
import { CLAUDE_COMMANDS, READ_FROM, READ_AT } from './claude-commands.generated.mjs';
|
|
42
|
+
|
|
43
|
+
export { CLAUDE_COMMANDS, READ_FROM, READ_AT };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Every spelling that reaches a command: its own name and the aliases Claude
|
|
47
|
+
* Code registers for it. `/cost` and `/stats` are theirs, for `/usage`. */
|
|
48
|
+
export function commandWords(cmd) {
|
|
49
|
+
return [cmd.name, ...(cmd.aliases ?? [])];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The line the menu prints beside the command. Claude Code's own shorter
|
|
54
|
+
* sentence where they wrote one for a menu, otherwise their full one.
|
|
55
|
+
*
|
|
56
|
+
* WITH THE EM DASHES TAKEN OUT, and that has to happen here rather than in the
|
|
57
|
+
* generated file. One of the eight carries one today: `/goal` reads "Set a goal
|
|
58
|
+
* — keep working until the condition is met", and that sentence is Claude
|
|
59
|
+
* Code's, read out of the installed binary at build time. Editing it where it
|
|
60
|
+
* lands would be undone by the next `npm run build` and would make the file
|
|
61
|
+
* stop matching the binary, which is the whole reason it is generated. So the
|
|
62
|
+
* record stays faithful and the MENU does the spelling. A comma, because these
|
|
63
|
+
* are all one short sentence with a pause in it. */
|
|
64
|
+
export function commandHint(cmd) {
|
|
65
|
+
return (cmd.menuDescription || cmd.description).replace(/\s*—\s*/g, ', ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The rows the menu draws for what has been typed so far, in her order.
|
|
70
|
+
*
|
|
71
|
+
* Prefix on the name first and on an alias second, which is the same rule
|
|
72
|
+
* modeMatches follows and for the same reason: a word appearing inside a
|
|
73
|
+
* longer word is a coincidence, and a menu that moves its cursor on a
|
|
74
|
+
* coincidence feels random. */
|
|
75
|
+
export function commandMatches(query) {
|
|
76
|
+
if (query === null || query === undefined) return [];
|
|
77
|
+
if (query === '') return [...CLAUDE_COMMANDS];
|
|
78
|
+
const first = CLAUDE_COMMANDS.filter((c) => c.name.startsWith(query));
|
|
79
|
+
const alias = CLAUDE_COMMANDS.filter(
|
|
80
|
+
(c) => !first.includes(c) && commandWords(c).some((w) => w.startsWith(query)),
|
|
81
|
+
);
|
|
82
|
+
return [...first, ...alias];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* What picking a row puts in the box. A trailing space, always, because every
|
|
87
|
+
* one of the eight either takes an argument or is harmless with none, and a
|
|
88
|
+
* caret sitting after the space is what tells somebody there is more to type.
|
|
89
|
+
* It is never sent by the pick: she presses Send, the same as any message. */
|
|
90
|
+
export function commandDraft(cmd) {
|
|
91
|
+
return `/${cmd.name} `;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* --------------------- and the other end, in the run ---------------------- */
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* WHETHER A REPLY IS ONE OF THE EIGHT, asked by the supervisor at the moment it
|
|
98
|
+
* decides what to hand the CLI.
|
|
99
|
+
*
|
|
100
|
+
* Returns the prompt to send verbatim, or null for an ordinary message. Null is
|
|
101
|
+
* the answer for everything that is not exactly one of these commands, because
|
|
102
|
+
* this decides whether a message SKIPS the brief, and a brief skipped by
|
|
103
|
+
* accident is a worker that does not know which row it is on.
|
|
104
|
+
*
|
|
105
|
+
* THE RULES, AND EACH ONE IS A THING THAT WOULD OTHERWISE GO WRONG:
|
|
106
|
+
*
|
|
107
|
+
* - The slash is the first character. A slash mid-sentence is a path or a
|
|
108
|
+
* date. Same rule as slashQuery in modes.ts.
|
|
109
|
+
* - The word after it is one of the eight, or one of their aliases. Not any
|
|
110
|
+
* command Claude Code has: the other 94 are not on the menu, so a person
|
|
111
|
+
* typing one has not been told it would run, and 70 of them cannot run
|
|
112
|
+
* outside an interactive session at all.
|
|
113
|
+
* - Everything after the word is the argument and is passed through
|
|
114
|
+
* untouched, including newlines, because `/goal <condition>` and
|
|
115
|
+
* `/compact <instructions>` are sentences.
|
|
116
|
+
* - The name is normalised to the command's own name, so `/cost` reaches the
|
|
117
|
+
* CLI as `/usage`. An alias is a spelling for us; the CLI takes both, and
|
|
118
|
+
* sending its own word is one less thing to be wrong about.
|
|
119
|
+
*/
|
|
120
|
+
export function commandPrompt(reply, nativeNames = []) {
|
|
121
|
+
if (typeof reply !== 'string') return null;
|
|
122
|
+
if (!reply.startsWith('/')) return null;
|
|
123
|
+
const m = reply.match(/^\/([a-z][a-z0-9:_-]*)(?:(\s[\s\S]*))?$/i);
|
|
124
|
+
if (!m) return null;
|
|
125
|
+
const typed = m[1].toLowerCase();
|
|
126
|
+
const word = typed === 'review' && nativeNames.includes('code-review') ? 'code-review' : typed;
|
|
127
|
+
const cmd = CLAUDE_COMMANDS.find((c) => commandWords(c).includes(word)) ?? (nativeNames.includes(word) ? { name: word } : null);
|
|
128
|
+
if (!cmd) return null;
|
|
129
|
+
const rest = (m[2] ?? '').trim();
|
|
130
|
+
return rest ? `/${cmd.name} ${rest}` : `/${cmd.name}`;
|
|
131
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// GENERATED BY scripts/read-claude-models.mjs. DO NOT EDIT BY HAND.
|
|
2
|
+
|
|
3
|
+
export declare const READ_FROM: string | null;
|
|
4
|
+
export declare const READ_AT: string;
|
|
5
|
+
|
|
6
|
+
export declare const CLAUDE_MODELS: ReadonlyArray<{
|
|
7
|
+
/** What Agentbox sends the CLI: an alias, never a version. */
|
|
8
|
+
alias: string;
|
|
9
|
+
/** What that alias resolved to on the Claude Code that built this. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Claude Code's own display name for that id, e.g. "Opus 5". */
|
|
12
|
+
label: string;
|
|
13
|
+
/** The level it thinks at when nobody picks one, out of the same catalog. */
|
|
14
|
+
defaultLevel: string | null;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// GENERATED BY scripts/read-claude-models.mjs. DO NOT EDIT BY HAND.
|
|
2
|
+
//
|
|
3
|
+
// Which model each alias really is, and Claude Code's own name for it, read
|
|
4
|
+
// out of the copy of Claude Code installed on the machine that built this.
|
|
5
|
+
// `npm run build` rewrites it. Committed so a machine with no Claude Code on
|
|
6
|
+
// it can still build, and so a version that points an alias at a different
|
|
7
|
+
// model shows up as a diff rather than as a wrong label on the picker.
|
|
8
|
+
//
|
|
9
|
+
// WHY IT IS READ AND NOT TYPED: labelling a model wrong is expensive, because
|
|
10
|
+
// Agentbox sends the alias, the CLI decides what the alias means, and a version
|
|
11
|
+
// number typed into a file of ours would look authoritative while being a
|
|
12
|
+
// guess. Running an old model by mistake is the failure being avoided.
|
|
13
|
+
|
|
14
|
+
export const READ_FROM = "2.1.283";
|
|
15
|
+
export const READ_AT = "2026-09-29";
|
|
16
|
+
|
|
17
|
+
export const CLAUDE_MODELS = [
|
|
18
|
+
{"alias":"opus","id":"claude-opus-5-5","label":"Opus 5.5","defaultLevel":"medium"},
|
|
19
|
+
{"alias":"claude-opus-5","id":"claude-opus-5","label":"Opus 5","defaultLevel":"high"},
|
|
20
|
+
{"alias":"sonnet","id":"claude-sonnet-5","label":"Sonnet 5","defaultLevel":"high"},
|
|
21
|
+
{"alias":"claude-sonnet-4-6","id":"claude-sonnet-4-6","label":"Sonnet 4.6","defaultLevel":null},
|
|
22
|
+
{"alias":"haiku","id":"claude-haiku-4-5","label":"Haiku 4.5","defaultLevel":null},
|
|
23
|
+
{"alias":"claude-3-5-haiku","id":"claude-3-5-haiku","label":"Haiku 3.5","defaultLevel":null},
|
|
24
|
+
{"alias":"fable","id":"claude-fable-5-1","label":"Fable 5.1","defaultLevel":"high"},
|
|
25
|
+
{"alias":"claude-fable-5","id":"claude-fable-5","label":"Fable 5","defaultLevel":"high"},
|
|
26
|
+
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Types for shared/claude-usage.mjs, which the renderer imports and which is
|
|
2
|
+
// plain JavaScript because main imports it too. Hand-written and short: the
|
|
3
|
+
// module is one parser and the timezone arithmetic under it.
|
|
4
|
+
//
|
|
5
|
+
// The words the panel prints moved to shared/usage.mjs on 2026-09-05, and their
|
|
6
|
+
// declarations went with them. `UsageLimit` is declared there too, because a
|
|
7
|
+
// limit is a limit whichever coding agent reported it.
|
|
8
|
+
|
|
9
|
+
import type { UsageLimit } from './usage.d.mts';
|
|
10
|
+
|
|
11
|
+
export declare function resetAt(text: string, now?: number, zoneNow?: string | null): number | null;
|
|
12
|
+
export declare function readUsage(text: string | null | undefined, now?: number, zoneNow?: string | null): UsageLimit[];
|
|
13
|
+
export declare function pillText(
|
|
14
|
+
limits: UsageLimit[] | null | undefined,
|
|
15
|
+
now?: number,
|
|
16
|
+
): { used: string; when: string | null } | null;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// WHAT IS LEFT OF HER LIMIT, OUT OF CLAUDE CODE'S OWN `/usage`.
|
|
2
|
+
//
|
|
3
|
+
// He is right that it is the number that decides how many agents get started,
|
|
4
|
+
// and right that going elsewhere to read it defeats the point of the inbox.
|
|
5
|
+
//
|
|
6
|
+
// THERE ARE THREE LIMITS, NOT ONE, and that is the first thing measuring changed
|
|
7
|
+
// about his ask. Verbatim off her Mac, 2026-08-31 16:47:
|
|
8
|
+
//
|
|
9
|
+
// Current session: 29% used · resets Aug 31 at 6:19pm (America/Los_Angeles)
|
|
10
|
+
// Current week (all models): 6% used · resets Sep 3 at 3:59pm (America/Los_Angeles)
|
|
11
|
+
// Current week (Fable): 2% used · resets Sep 3 at 3:59pm (America/Los_Angeles)
|
|
12
|
+
//
|
|
13
|
+
// AND IT IS SLOW. Timed twice on the same machine: 23.2s and 28.0s. So this can
|
|
14
|
+
// never be live, and nothing that reads it may block a window being drawn.
|
|
15
|
+
//
|
|
16
|
+
// This file is the PURE half: the text in, the three limits out. It is pure so
|
|
17
|
+
// the reading is pinned by tests rather than by a screenshot. Running the
|
|
18
|
+
// command is main/claude-usage.mjs.
|
|
19
|
+
//
|
|
20
|
+
// THE WORDS THE PANEL PRINTS LEFT THIS FILE ON 2026-09-05, and they went to
|
|
21
|
+
// shared/usage.mjs. What is Claude Code's is still here: this text format, the
|
|
22
|
+
// timezone stamp it prints, and the refusal to count down from a stamp computed
|
|
23
|
+
// somewhere else. What belongs to a LIMIT, any limit, is next door.
|
|
24
|
+
|
|
25
|
+
import { instantIn, localZone, sessionLimit, untilReset, wallIn } from './usage.mjs';
|
|
26
|
+
|
|
27
|
+
/** One limit, as the CLI reported it. */
|
|
28
|
+
// { span, qualifier, name, percent, resetsAt|null, resetsText, resetsOn, zone }
|
|
29
|
+
|
|
30
|
+
const LINE = /^Current\s+(session|week)(?:\s*\(([^)]+)\))?\s*:\s*(\d+)%\s*used(?:\s*·\s*resets\s+(.+?))?\s*$/i;
|
|
31
|
+
// THE MINUTES ARE OPTIONAL, AND THAT IS WHY HER CORNER WENT QUIET. Claude Code
|
|
32
|
+
// drops the ":00" when a reset lands on the hour, so the stamp on her Mac read
|
|
33
|
+
// "Sep 1 at 4pm" and this refused it. Refusing it took the countdown out of the
|
|
34
|
+
// pill AND out of the tooltip, which left the wordless "41%" she photographed
|
|
35
|
+
// and could not read. It is one hour in every five.
|
|
36
|
+
//
|
|
37
|
+
// THE AM/PM HALF STAYS REQUIRED. An hour on its own is ambiguous by twelve
|
|
38
|
+
// hours, and the standing rule in this file is to refuse rather than guess.
|
|
39
|
+
const RESET = /^([A-Z][a-z]{2})\s+(\d{1,2})\s+at\s+(\d{1,2})(?::(\d{2}))?\s*([ap])m(?:\s*\(([^)]+)\))?/i;
|
|
40
|
+
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* WHEN A RESET ACTUALLY IS, from the stamp the CLI prints.
|
|
44
|
+
*
|
|
45
|
+
* The stamp has no year on it, so the year is taken from `now` and rolled
|
|
46
|
+
* forward when that would put the reset more than a day in the past, which is
|
|
47
|
+
* the December-to-January case and the only one it can be.
|
|
48
|
+
*
|
|
49
|
+
* IT REFUSES RATHER THAN GUESSES WHEN THE ZONE IS NOT THIS MACHINE'S. Claude Code
|
|
50
|
+
* prints the zone it computed the stamp in. When that is the zone this machine is
|
|
51
|
+
* in, reading "6:19pm" as local time is exactly right. When it is not, the
|
|
52
|
+
* difference is however many hours, and a countdown built on that would be
|
|
53
|
+
* confidently wrong. So it returns null and the caller prints the clock time
|
|
54
|
+
* instead, which is true in either case.
|
|
55
|
+
*
|
|
56
|
+
* AND THE CLOCK TIME IS READ IN THAT ZONE, NOT IN THE PROCESS'S (2026-09-04).
|
|
57
|
+
* This function has taken `zoneNow` since it was written, and used it for the
|
|
58
|
+
* refusal above and for nothing else: the instant itself came out of
|
|
59
|
+
* `new Date(y, m, d, h, min)`, which reads those fields in whatever zone the
|
|
60
|
+
* PROCESS happens to be in. On her Mac the two are the same zone and every
|
|
61
|
+
* answer was right, which is why it survived. Anywhere else it is silently out
|
|
62
|
+
* by the difference: running this suite in Asia/Tbilisi against her verbatim
|
|
63
|
+
* America/Los_Angeles reading put every reset eleven hours early, which turned
|
|
64
|
+
* `pillText`'s "2h 14m left" into "any moment" and made the panel print
|
|
65
|
+
* "Resets Sep 1 at 4pm" for a reset that was today. A function handed the
|
|
66
|
+
* moment AND the zone and then reading one of them off the machine cannot be
|
|
67
|
+
* tested, and it rots the day it leaves the desk it was written on.
|
|
68
|
+
*/
|
|
69
|
+
export function resetAt(text, now = Date.now(), zoneNow = localZone()) {
|
|
70
|
+
const m = RESET.exec(String(text ?? '').trim());
|
|
71
|
+
if (!m) return null;
|
|
72
|
+
const [, mon, day, hour12, minute, half, zone] = m;
|
|
73
|
+
if (zone && zoneNow && zone !== zoneNow) return null;
|
|
74
|
+
const month = MONTHS.indexOf(mon.toLowerCase());
|
|
75
|
+
if (month < 0) return null;
|
|
76
|
+
let hour = Number(hour12) % 12;
|
|
77
|
+
if (half.toLowerCase() === 'p') hour += 12;
|
|
78
|
+
// Absent minutes mean the hour exactly, which is what the CLI means by
|
|
79
|
+
// printing "4pm". It is a default for a stamp that omitted them, never a
|
|
80
|
+
// guess at ones it printed and we failed to read.
|
|
81
|
+
const min = minute === undefined ? 0 : Number(minute);
|
|
82
|
+
// The stamp carries no year, so it comes from `now` READ IN THE SAME ZONE the
|
|
83
|
+
// clock time is about to be read in. Taking the year from one zone and the
|
|
84
|
+
// day from another is a one-day error for the few hours a year they disagree.
|
|
85
|
+
const year = wallIn(now, zoneNow)?.year ?? new Date(now).getFullYear();
|
|
86
|
+
let at = instantIn(year, month, Number(day), hour, min, zoneNow);
|
|
87
|
+
// A reset a long way behind us is last year's date read as this year's.
|
|
88
|
+
if (at < now - 24 * 3_600_000) at = instantIn(year + 1, month, Number(day), hour, min, zoneNow);
|
|
89
|
+
return at;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* WHAT EACH LIMIT IS CALLED, IN HER WORDS. One copy, because the sentence the
|
|
94
|
+
* corner reads aloud and the panel's rows have to agree word for word: they are
|
|
95
|
+
* on the screen at the same moment, one of them read aloud and one of them read.
|
|
96
|
+
*
|
|
97
|
+
* SO "all models" IS DROPPED. It is the CLI's qualifier on the week line and it
|
|
98
|
+
* means every model, which is to say it is not a qualifier at all. Printed, it
|
|
99
|
+
* put "This week, all models" directly above "This week, Fable", where the
|
|
100
|
+
* first reads as one subset beside another instead of as the total it is. The
|
|
101
|
+
* qualifier is still kept on the limit itself and any OTHER one is still
|
|
102
|
+
* printed, so a fourth line the CLI invents tomorrow arrives named.
|
|
103
|
+
*
|
|
104
|
+
* IT IS STAMPED ONTO THE LIMIT HERE RATHER THAN DERIVED WHERE THE PANEL DRAWS
|
|
105
|
+
* (2026-09-05). These are Claude Code's spans and Claude Code's qualifiers, and
|
|
106
|
+
* the panel draws Codex's limits now too; a namer that ran over every reading
|
|
107
|
+
* would have had one branch for "session", one for "week" and no honest answer
|
|
108
|
+
* for anything else -- which is how "This week, Fable", a Claude model, could
|
|
109
|
+
* have appeared over a Codex reading. Each reader names its own limits. */
|
|
110
|
+
function limitName(span, qualifier) {
|
|
111
|
+
if (span === 'session') return 'This session';
|
|
112
|
+
const q = qualifier && qualifier.toLowerCase() !== 'all models' ? qualifier : null;
|
|
113
|
+
return q ? `This week, ${q}` : 'This week';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* EVERY LIMIT THE COMMAND REPORTED, in the order it reported them.
|
|
118
|
+
*
|
|
119
|
+
* Unknown lines are ignored rather than guessed at: the command prints a whole
|
|
120
|
+
* page (requests, sessions, what is contributing to the limits) and this only
|
|
121
|
+
* claims to understand the lines that say a percentage.
|
|
122
|
+
*/
|
|
123
|
+
export function readUsage(text, now = Date.now(), zoneNow = localZone()) {
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const raw of String(text ?? '').split('\n')) {
|
|
126
|
+
const m = LINE.exec(raw.trim());
|
|
127
|
+
if (!m) continue;
|
|
128
|
+
const [, span, qualifier, percent, resets] = m;
|
|
129
|
+
const reset = RESET.exec(String(resets ?? '').trim());
|
|
130
|
+
const kind = span.toLowerCase();
|
|
131
|
+
const named = qualifier ? qualifier.trim() : null;
|
|
132
|
+
out.push({
|
|
133
|
+
// `session` is the five hour one he asked for. The week lines carry a
|
|
134
|
+
// qualifier ("all models", "Fable") and it is kept as the CLI wrote it.
|
|
135
|
+
span: kind,
|
|
136
|
+
qualifier: named,
|
|
137
|
+
name: limitName(kind, named),
|
|
138
|
+
percent: Math.max(0, Math.min(100, Number(percent))),
|
|
139
|
+
// The whole stamp, always, because it is true even when the countdown
|
|
140
|
+
// cannot be computed.
|
|
141
|
+
// Written back the way the CLI wrote it: "6:19pm" keeps its minutes and
|
|
142
|
+
// "4pm" does not grow a ":00" it never had.
|
|
143
|
+
resetsText: reset ? `${reset[3]}${reset[4] ? `:${reset[4]}` : ''}${reset[5].toLowerCase()}m` : null,
|
|
144
|
+
// AND THE DAY IT FALLS ON, kept separately. The clock time alone is what
|
|
145
|
+
// the panel used to have, and on the two WEEK rows "4pm" reads as today
|
|
146
|
+
// when it is Thursday's. Claude Code prints the day, so we keep the day
|
|
147
|
+
// to print. "Sep 3", as the command wrote it.
|
|
148
|
+
resetsOn: reset ? `${reset[1]} ${Number(reset[2])}` : null,
|
|
149
|
+
resetsAt: resets ? resetAt(resets, now, zoneNow) : null,
|
|
150
|
+
zone: reset?.[6] ?? null,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* WHAT THE PILL SAYS. Null when there is nothing honest to print, and then
|
|
158
|
+
* nothing is drawn at all rather than a pill saying it does not know.
|
|
159
|
+
*
|
|
160
|
+
* Two parts, which is what he asked for: "Even a small pill with percent used
|
|
161
|
+
* and time left would do it."
|
|
162
|
+
*/
|
|
163
|
+
export function pillText(limits, now = Date.now()) {
|
|
164
|
+
const session = sessionLimit(limits);
|
|
165
|
+
if (!session) return null;
|
|
166
|
+
const used = `${session.percent}%`;
|
|
167
|
+
if (session.resetsAt) return { used, when: untilReset(session.resetsAt - now) };
|
|
168
|
+
// No countdown to be had, either because the stamp was missing or because it
|
|
169
|
+
// was computed in another timezone. The clock time is still true.
|
|
170
|
+
if (session.resetsText) return { used, when: `resets ${session.resetsText}` };
|
|
171
|
+
return { used, when: null };
|
|
172
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Only operations PowerUp actually dispatches. Paths and prose remain messages.
|
|
2
|
+
export const CODEX_COMPACT = Object.freeze({ name: 'compact', description: 'Free up context in this conversation', menuDescription: null, whole: true, argumentHint: null, aliases: [] });
|
|
3
|
+
export function codexCommand(text) {
|
|
4
|
+
if (typeof text !== 'string') return null;
|
|
5
|
+
const value = text.trim();
|
|
6
|
+
if (!/^\/compact(?:\s|$)/i.test(value)) return null;
|
|
7
|
+
return { name: 'compact', valid: /^\/compact$/i.test(value) };
|
|
8
|
+
}
|
|
9
|
+
export const COMPACTION_COPY = Object.freeze({
|
|
10
|
+
running: 'Compacting this conversation…',
|
|
11
|
+
done: 'Conversation compacted. You can continue this task.',
|
|
12
|
+
failed: 'Couldn’t compact this conversation. Try again.',
|
|
13
|
+
busy: 'Wait for the current response to finish and pause any active goal, then run /compact.',
|
|
14
|
+
unavailable: 'Compaction isn’t available with this Codex version.',
|
|
15
|
+
missing: 'This task doesn’t have a Codex conversation to compact yet.',
|
|
16
|
+
invalid: 'Run /compact on its own. Your draft has been kept.',
|
|
17
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Types for shared/codex-import.mjs, so the renderer can import it under
|
|
2
|
+
// strict TypeScript the way it does the other shared modules.
|
|
3
|
+
export const CODEX_IMPORT_LABEL: string;
|
|
4
|
+
export const CODEX_MIRROR_LABEL: string;
|
|
5
|
+
export const NOT_IMPORTED_LABEL: string;
|
|
6
|
+
export const IMPORT_KIND: string;
|
|
7
|
+
export function codexLabel(id: string | null | undefined): string;
|
|
8
|
+
export function codexIdOf(item: { labels?: string[] } | null | undefined): string | null;
|
|
9
|
+
export function isCodexImportRow(item: { labels?: string[]; status?: string } | null | undefined): boolean;
|
|
10
|
+
export function isNotImportedRow(item: { labels?: string[]; status?: string } | null | undefined): boolean;
|
|
11
|
+
export function isCodexMirrorRow(item: { labels?: string[] } | null | undefined): boolean;
|
|
12
|
+
export function whenPhrase(when: number | null | undefined, now?: number): string;
|
|
13
|
+
export function plainQuote(text: string | null | undefined): string;
|
|
14
|
+
export function threadFactsLine(thread: CodexThreadLike, now?: number): string;
|
|
15
|
+
export const FIRST_LINE_BUDGET: number;
|
|
16
|
+
export function importFirstLine(thread: CodexThreadLike, opts?: { projectName?: string | null; now?: number }): string;
|
|
17
|
+
export function productsForThread<P extends { repoPath?: string | null }>(thread: CodexThreadLike, products?: P[]): P[];
|
|
18
|
+
export interface CodexThreadLike {
|
|
19
|
+
id?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
folder?: string;
|
|
22
|
+
when?: number;
|
|
23
|
+
prompt?: string;
|
|
24
|
+
last?: string;
|
|
25
|
+
short?: string;
|
|
26
|
+
startedAt?: number;
|
|
27
|
+
lastActive?: number;
|
|
28
|
+
turns?: number;
|
|
29
|
+
images?: number;
|
|
30
|
+
live?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export function codexImportRow(thread: CodexThreadLike, opts?: { projectName?: string | null; now?: number }): {
|
|
33
|
+
title: string; body: string; kind: string; priority: number; labels: string[];
|
|
34
|
+
};
|
|
35
|
+
export function codexMirrorPatch(thread: CodexThreadLike): { body: string; result: string; kind: string; labels: string[] };
|
|
36
|
+
export function importChoice(answer: string | null | undefined): 'yes' | 'no' | null;
|
|
37
|
+
export function whereThreadLands<P extends { repoPath?: string | null }>(thread: CodexThreadLike, products?: P[]): P | null;
|
|
38
|
+
export function threadsNeedingRows<T extends CodexThreadLike>(threads: T[], existingItems?: Array<{ labels?: string[] }>): T[];
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// THE ROW THAT ASKS WHETHER A CODEX CONVERSATION COMES IN, and what her yes and
|
|
2
|
+
// her no do to it.
|
|
3
|
+
//
|
|
4
|
+
// THE TITLE IS CODEX'S OWN. The row is called what Codex called it, so she
|
|
5
|
+
// finds it by the name she remembers from the other tab.
|
|
6
|
+
//
|
|
7
|
+
// THE FIRST LINE OF THE BODY SAYS WHERE IT WOULD GO AND WHEN IT WAS. That is
|
|
8
|
+
// the line the inbox row prints under the title, so it has to carry the
|
|
9
|
+
// whole question on its own: "Import into the app? A Codex conversation
|
|
10
|
+
// from 7:22 PM today."
|
|
11
|
+
//
|
|
12
|
+
// THE BODY ENDS IN THE APP'S OWN OPTIONS. Two options, Import into X and Not
|
|
13
|
+
// now, so opening the row draws the strip and a 1 or a 2 answers it exactly as
|
|
14
|
+
// Y and N do on the row.
|
|
15
|
+
//
|
|
16
|
+
// NOTHING IS RESUMED. She chose that on 08-29 for Claude Code threads and it
|
|
17
|
+
// holds here: a yes makes a row she can READ, with her prompt and Codex's
|
|
18
|
+
// last message, and replies still happen in Codex.
|
|
19
|
+
//
|
|
20
|
+
// A NO NEVER LOSES ONE.So a no closes the row under its own label, Closed shows
|
|
21
|
+
// it under "Not imported" with an Import key, and the ⌘K import card lists it
|
|
22
|
+
// too.
|
|
23
|
+
//
|
|
24
|
+
// Pure on purpose, like shared/agent-import.mjs beside it: the words are tested
|
|
25
|
+
// rather than eyeballed, and main/store.mjs does the writing.
|
|
26
|
+
|
|
27
|
+
/** On the row while it is asking. */
|
|
28
|
+
import { Name } from '../shared/product-name.mjs';
|
|
29
|
+
export const CODEX_IMPORT_LABEL = 'codex-import';
|
|
30
|
+
/** On the row once it is hers to read. */
|
|
31
|
+
export const CODEX_MIRROR_LABEL = 'codex';
|
|
32
|
+
/** On a row she said not now to. */
|
|
33
|
+
export const NOT_IMPORTED_LABEL = 'not-imported';
|
|
34
|
+
/** And which conversation, on all three. Codex's own thread id. */
|
|
35
|
+
export const codexLabel = (id) => `codex:${String(id ?? '').trim()}`;
|
|
36
|
+
|
|
37
|
+
/** The kind the asking row wears, so the list can draw it differently. */
|
|
38
|
+
export const IMPORT_KIND = 'import';
|
|
39
|
+
|
|
40
|
+
export function codexIdOf(item) {
|
|
41
|
+
for (const label of item?.labels ?? []) {
|
|
42
|
+
if (typeof label === 'string' && label.startsWith('codex:')) return label.slice(6);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
export const isCodexImportRow = (item) => (item?.labels ?? []).includes(CODEX_IMPORT_LABEL) && item?.status !== 'done';
|
|
47
|
+
export const isNotImportedRow = (item) => (item?.labels ?? []).includes(NOT_IMPORTED_LABEL) && item?.status === 'done';
|
|
48
|
+
export const isCodexMirrorRow = (item) => (item?.labels ?? []).includes(CODEX_MIRROR_LABEL) && !!codexIdOf(item);
|
|
49
|
+
|
|
50
|
+
/** "7:22 PM today", "yesterday at 7:22 PM", "Friday at 7:22 PM", else the date. */
|
|
51
|
+
export function whenPhrase(when, now = Date.now()) {
|
|
52
|
+
const then = new Date(Number(when) || 0);
|
|
53
|
+
if (!when || !Number.isFinite(then.getTime())) return '';
|
|
54
|
+
const time = then.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
|
55
|
+
const midnight = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
56
|
+
const days = Math.round((midnight(new Date(now)) - midnight(then)) / 86400000);
|
|
57
|
+
if (days <= 0) return `${time} today`;
|
|
58
|
+
if (days === 1) return `yesterday at ${time}`;
|
|
59
|
+
if (days < 7) return `${then.toLocaleDateString('en-US', { weekday: 'long' })} at ${time}`;
|
|
60
|
+
return then.toLocaleDateString('en-US', { month: 'long', day: 'numeric' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* WHAT CODEX WROTE, IN WORDS THAT WORK FROM A ROW (2026-09-16).
|
|
65
|
+
*
|
|
66
|
+
* Codex answers in markdown aimed at its own window: a picture at a path in a
|
|
67
|
+
* temporary folder, a link to a preview server it started on this Mac, a link
|
|
68
|
+
* to a notes file in its worktree.
|
|
69
|
+
*
|
|
70
|
+
* So: pictures are left out. A link to the open web stays a link, because it
|
|
71
|
+
* works from anywhere (her own cofounder.co link does). A link to this Mac, a
|
|
72
|
+
* file path or a localhost address, keeps its words and loses its target, and
|
|
73
|
+
* a line that was nothing but such a link goes entirely, since "Open the
|
|
74
|
+
* interactive preview." with nothing to open is the confusing part. */
|
|
75
|
+
const isWebLink = (target) => /^https?:\/\//i.test(target) && !/^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])([:/]|$)/i.test(target);
|
|
76
|
+
export function plainQuote(text) {
|
|
77
|
+
const lines = String(text ?? '').split('\n').map((raw) => {
|
|
78
|
+
const line = raw.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
|
|
79
|
+
const only = line.trim().match(/^\[([^\]]+)\]\(\s*([^)\s]+)[^)]*\)[.!]?$/);
|
|
80
|
+
if (only && !isWebLink(only[2])) return null;
|
|
81
|
+
return line.replace(/\[([^\]]+)\]\(\s*([^)\s]+)[^)]*\)/g, (whole, words, target) => (isWebLink(target) ? whole : words));
|
|
82
|
+
});
|
|
83
|
+
return lines.filter((l) => l !== null).join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One paragraph of a longer text. Headings lose their hashes and become
|
|
88
|
+
* sentences, so "### 1. Folio" reads as "1. Folio." inside the line. */
|
|
89
|
+
const clip = (text, max) => {
|
|
90
|
+
const t = plainQuote(text)
|
|
91
|
+
.replace(/^\s*#{1,6}\s*(.+?)\s*$/gm, (_m, h) => (/[.!?:]$/.test(h) ? h : `${h}.`))
|
|
92
|
+
.replace(/\s+/g, ' ')
|
|
93
|
+
.trim();
|
|
94
|
+
return t.length > max ? `${t.slice(0, max - 1).trimEnd()}…` : t;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* THE LINE THAT SAYS WHAT THE CONVERSATION IS, IN FACTS. Where it ran, how
|
|
101
|
+
* long it has gone on, what she attached, and whether Codex is still in
|
|
102
|
+
* it.*/
|
|
103
|
+
export function threadFactsLine(thread, now = Date.now()) {
|
|
104
|
+
const where = String(thread?.short || thread?.folder || '').trim();
|
|
105
|
+
const startedAt = Number(thread?.startedAt) || 0;
|
|
106
|
+
const when = Number(thread?.when) || 0;
|
|
107
|
+
const lastActive = Number(thread?.lastActive) || 0;
|
|
108
|
+
if (!where && !startedAt && !when) return '';
|
|
109
|
+
// One sentence for when and where. A conversation that went on for a while
|
|
110
|
+
// says when it started and when it was last touched; a short one says only
|
|
111
|
+
// when it was.
|
|
112
|
+
const longRun = startedAt && Math.abs(when - startedAt) > 60_000;
|
|
113
|
+
const opening = longRun ? `started ${whenPhrase(startedAt, now)}` : (when || startedAt) ? `from ${whenPhrase(when || startedAt, now)}` : '';
|
|
114
|
+
let tail = '.';
|
|
115
|
+
if (thread?.live) tail = ', still going.';
|
|
116
|
+
else if (lastActive && startedAt && lastActive - startedAt > 60_000) tail = `, last active ${whenPhrase(lastActive, now)}.`;
|
|
117
|
+
const bits = [`A Codex conversation${opening ? ` ${opening}` : ''}${where ? ` in ${where}` : ''}${tail}`];
|
|
118
|
+
const turns = Number(thread?.turns) || 0;
|
|
119
|
+
const images = Number(thread?.images) || 0;
|
|
120
|
+
if (turns || images) {
|
|
121
|
+
bits.push(`${turns ? plural(turns, 'turn', 'turns') : ''}${turns && images ? ', ' : ''}${images ? plural(images, 'screenshot attached', 'screenshots attached') : ''}.`);
|
|
122
|
+
}
|
|
123
|
+
return bits.join(' ');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* THE FIRST LINE IS WHAT SHE ASKED CODEX, because that is what the inbox row
|
|
128
|
+
* prints under the title and it is the detail she approves from. The whole
|
|
129
|
+
* line stays inside the row's 112 character budget, so the quote is cut to
|
|
130
|
+
* fit and the full prompt follows under it. */
|
|
131
|
+
export const FIRST_LINE_BUDGET = 110;
|
|
132
|
+
export function importFirstLine(thread, { projectName, now = Date.now() } = {}) {
|
|
133
|
+
const name = String(projectName || '').trim() || 'this project';
|
|
134
|
+
// One line of plain words: a web link keeps its words here and loses its
|
|
135
|
+
// address, because the inbox line draws text and not markdown.
|
|
136
|
+
const asked = plainQuote(thread?.prompt).replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/\s+/g, ' ').trim().replace(/[.!?…]+$/, '');
|
|
137
|
+
if (!asked) {
|
|
138
|
+
const when = whenPhrase(thread?.when, now);
|
|
139
|
+
return `Import into ${name}? A Codex conversation${when ? ` from ${when}` : ''}.`;
|
|
140
|
+
}
|
|
141
|
+
const lead = `Import into ${name}? You asked Codex: "`;
|
|
142
|
+
const room = FIRST_LINE_BUDGET - lead.length - 2; // the closing quote and the full stop
|
|
143
|
+
const quote = asked.length > room ? `${asked.slice(0, Math.max(1, room - 1)).trimEnd()}…` : asked;
|
|
144
|
+
return `${lead}${quote}".`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* THE ASKING ROW'S WORDS. The first line carries the question and is what the
|
|
149
|
+
* inbox row prints; the facts line, her prompt and Codex's last message are the
|
|
150
|
+
* detail she approves from. */
|
|
151
|
+
export function codexImportRow(thread, { projectName, now = Date.now() } = {}) {
|
|
152
|
+
const title = String(thread?.title || '').trim();
|
|
153
|
+
const name = String(projectName || '').trim() || 'this project';
|
|
154
|
+
const first = importFirstLine(thread, { projectName: name, now });
|
|
155
|
+
const facts = threadFactsLine(thread, now);
|
|
156
|
+
const prompt = clip(thread?.prompt, 700);
|
|
157
|
+
const last = clip(thread?.last, 700);
|
|
158
|
+
const body = [
|
|
159
|
+
`**${first}**`,
|
|
160
|
+
...(facts ? [facts] : []),
|
|
161
|
+
...(prompt ? [`You asked: ${prompt}`] : [`${Name} could not read what you asked; open it in Codex to see.`]),
|
|
162
|
+
...(last ? [`Codex last said: ${last}`] : ['Codex has not answered yet.']),
|
|
163
|
+
'Yes makes it a row here you can read. No puts it in Closed under Not imported, where it can be brought in any time. Nothing is moved.',
|
|
164
|
+
`## Options\n1. Import into ${name}\n2. Not now`,
|
|
165
|
+
].join('\n\n');
|
|
166
|
+
return {
|
|
167
|
+
title,
|
|
168
|
+
body,
|
|
169
|
+
kind: IMPORT_KIND,
|
|
170
|
+
priority: 5,
|
|
171
|
+
labels: [CODEX_IMPORT_LABEL, codexLabel(thread?.id)],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* THE ROW ONCE SHE SAID YES. Her prompt is the body and Codex's last message
|
|
177
|
+
* is the result, so the thread reads You, then Codex, the way it did over
|
|
178
|
+
* there. The result carries the one line that says where replies go. */
|
|
179
|
+
export function codexMirrorPatch(thread) {
|
|
180
|
+
const prompt = plainQuote(thread?.prompt);
|
|
181
|
+
const last = plainQuote(thread?.last);
|
|
182
|
+
return {
|
|
183
|
+
body: prompt || String(thread?.title ?? '').trim(),
|
|
184
|
+
result: last ? `${last}\n\nRead from Codex. Replies happen there.` : 'Codex has not answered yet.\n\nRead from Codex. Replies happen there.',
|
|
185
|
+
kind: 'directive',
|
|
186
|
+
labels: [CODEX_MIRROR_LABEL, codexLabel(thread?.id)],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* WHAT HER ANSWER MEANS. A key on the row sends the word; the strip sends
|
|
192
|
+
* "Option 1: Import into X"; a typed reply can be either. Null is not an
|
|
193
|
+
* answer to this row and falls through to the ordinary reply path. */
|
|
194
|
+
export function importChoice(answer) {
|
|
195
|
+
const a = String(answer ?? '').trim().toLowerCase();
|
|
196
|
+
if (!a) return null;
|
|
197
|
+
if (/^option\s*1\b/.test(a) || /^(y|yes|import)\b/.test(a)) return 'yes';
|
|
198
|
+
if (/^option\s*2\b/.test(a) || /^(n|no|not now|skip|decline)\b/.test(a)) return 'no';
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* WHICH PROJECT A CONVERSATION BELONGS TO: the one whose folder it ran in,
|
|
204
|
+
* her rule from 08-25. Null when nobody points at that folder. */
|
|
205
|
+
export function productsForThread(thread, products = []) {
|
|
206
|
+
const key = String(thread?.folder ?? '').replace(/\/+$/, '');
|
|
207
|
+
if (!key) return [];
|
|
208
|
+
return products.filter((p) => String(p?.repoPath ?? '').replace(/\/+$/, '') === key);
|
|
209
|
+
}
|
|
210
|
+
export function whereThreadLands(thread, products = []) {
|
|
211
|
+
return productsForThread(thread, products)[0] ?? null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* WHICH CONVERSATIONS HAVE NO ROW YET, anywhere. A row asking, a row she
|
|
216
|
+
* imported and a row she declined all carry `codex:<id>`, and any of the
|
|
217
|
+
* three is a reason to file nothing. */
|
|
218
|
+
export function threadsNeedingRows(threads, existingItems = []) {
|
|
219
|
+
const already = new Set();
|
|
220
|
+
for (const item of existingItems) {
|
|
221
|
+
const id = codexIdOf(item);
|
|
222
|
+
if (id) already.add(id);
|
|
223
|
+
}
|
|
224
|
+
const byId = new Map();
|
|
225
|
+
for (const t of threads ?? []) {
|
|
226
|
+
const id = String(t?.id ?? '').trim();
|
|
227
|
+
if (!id || already.has(id) || byId.has(id)) continue;
|
|
228
|
+
if (!String(t?.title ?? '').trim()) continue;
|
|
229
|
+
byId.set(id, t);
|
|
230
|
+
}
|
|
231
|
+
return [...byId.values()];
|
|
232
|
+
}
|