agentbox-app 0.1.7 → 0.1.11
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/README.md +209 -55
- package/bin/agentbox.mjs +6 -0
- package/briefs/message-rules.md +13 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +9 -10
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +114 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +31 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +11 -11
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +17 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +68 -8
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +193 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +6 -3
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +1 -1
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +92 -54
- package/main/ipc.mjs +316 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +47 -0
- package/main/main.mjs +119 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +20 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +114 -51
- package/main/serve.mjs +67 -5
- package/main/settings.mjs +223 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +47 -2
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +235 -9
- package/main/supervisor.mjs +1444 -406
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +74 -13
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +2 -1
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +9 -11
- package/mcp/core/work.mjs +91 -25
- package/mcp/tools.mjs +12 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-B-2Az952.css +32 -0
- package/renderer/dist/assets/index-D6qncgai.js +1168 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +38 -15
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +37 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +4 -4
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +5 -5
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +22 -3
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +6 -0
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +39 -18
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +255 -27
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- 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/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-B7hhfHrT.js +0 -684
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- 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/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/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- 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 +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- package/renderer/dist/emulator/provenance.json +0 -6
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// PURE. EVERY KEY AGENTBOX HAS, IN THE ORDER A PERSON MEETS THEM.
|
|
2
|
+
//
|
|
3
|
+
// A shortcuts page in settings, because onboarding a non-technical tester
|
|
4
|
+
// meant saying each key out loud, one at a time: the app had a switch for
|
|
5
|
+
// whether it WHISPERS its keys and no page anywhere that says what they are.
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS IS A FILE AND NOT MARKUP IN THE COMPONENT. A page that prints a key
|
|
8
|
+
// the handler does not have is worse than no page at all — that is the rule
|
|
9
|
+
// `list-rules.ts` already keeps for the hint on a row, and the reason it holds
|
|
10
|
+
// even harder here is that a hint is read for a second and a reference page is
|
|
11
|
+
// believed. So the list is a value, and
|
|
12
|
+
// `tests/the-shortcuts-page-lists-keys-that-work.test.mjs` reads every entry
|
|
13
|
+
// below against the branch in App.tsx (or main/main.mjs, or Compose.tsx) that
|
|
14
|
+
// actually runs it. A key added here without a handler fails the suite.
|
|
15
|
+
//
|
|
16
|
+
// WHY IT IS IN shared/ AND PLAIN ESM (w-7ec8553e23, 2026-10-04). It lived in
|
|
17
|
+
// renderer/src as TypeScript until an agent told the user to "Press S on this
|
|
18
|
+
// task" three days after S stopped scheduling: nothing a session is handed
|
|
19
|
+
// said what the keys are, so it went by an old memory. The supervisor now
|
|
20
|
+
// hands every run `keysForAgents()`, read off this same list, and main cannot
|
|
21
|
+
// import TypeScript. Types are in shortcuts.d.mts beside it.
|
|
22
|
+
//
|
|
23
|
+
// ONE SENTENCE A ROW, AND IT IS SHORT (w-1bc916a880). Every
|
|
24
|
+
// sentence here is now under seventy characters and the test enforces it. A row
|
|
25
|
+
// that wants a second idea becomes its own row, or it does not go on the page.
|
|
26
|
+
//
|
|
27
|
+
// AND NO KEY HIDES INSIDE A SENTENCE.
|
|
28
|
+
//
|
|
29
|
+
// HOW IT IS GROUPED, and it is not how the code is arranged. The code has one
|
|
30
|
+
// switch for the list, one branch for an open task, a chord block above both
|
|
31
|
+
// and a main-process handler under all of it. Nobody thinks in those. Four
|
|
32
|
+
// headings, and every key sits under the one that matches when she would want
|
|
33
|
+
// it: in your inbox, in a task you have opened, closing and scheduling and
|
|
34
|
+
// undoing, anywhere in the app.
|
|
35
|
+
//
|
|
36
|
+
// EVERY HEADING IS LITERAL AND THERE IS NO SUBHEADING AT ALL (w-1bc916a880,
|
|
37
|
+
// second round).
|
|
38
|
+
//
|
|
39
|
+
// The plainest point first: that heading named one of the two
|
|
40
|
+
// directions its own rows draw. Three of the four were figures of speech, and a
|
|
41
|
+
// figure of speech is a second thing to decode on a page whose only job is to
|
|
42
|
+
// be looked up. So a heading now says WHEN the keys under it work, in the words
|
|
43
|
+
// the app itself uses for those places, and the one group that cannot be named
|
|
44
|
+
// that way is named by what its keys do instead, because all four of its keys
|
|
45
|
+
// work in both places.
|
|
46
|
+
//
|
|
47
|
+
// What the three of them carried, and where it went:
|
|
48
|
+
//
|
|
49
|
+
// "In Scheduled, E brings a task back instead of closing it." This was the
|
|
50
|
+
// most confusing one. It is also the same class of thing as G, B and
|
|
51
|
+
// the import letters below: a behaviour that only happens in one view. The
|
|
52
|
+
// page's existing rule is that those stay off it, so this now obeys the rule
|
|
53
|
+
// the rest of the file already kept.
|
|
54
|
+
//
|
|
55
|
+
// The two ↵ rows say what ↵ does when it does something.
|
|
56
|
+
//
|
|
57
|
+
// "Anything without a key of its own is in ⌘K by name." The lede at the top
|
|
58
|
+
// of the page already says this, so it was the same sentence twice.
|
|
59
|
+
//
|
|
60
|
+
// WHAT IS DELIBERATELY NOT HERE, so nobody adds it back as an oversight:
|
|
61
|
+
//
|
|
62
|
+
// ⌘1..4, for PRIORITY or for anything else. Priority was removed deliberately
|
|
63
|
+
// and that handler no longer fires. The chord then went to a section of the
|
|
64
|
+
// sidebar until 2026-10-02, and that is gone as well: Tab moves along the tabs
|
|
65
|
+
// now and is listed above.
|
|
66
|
+
//
|
|
67
|
+
// Y, N and I on a row in the list. Real since w-9741ed0e0b, but they answer
|
|
68
|
+
// a Codex conversation's import row and are dead on every other row in the
|
|
69
|
+
// app. A reference page that lists a key which does nothing on nineteen
|
|
70
|
+
// screens out of twenty teaches somebody that the app ignores them.
|
|
71
|
+
//
|
|
72
|
+
// ⇥ inside the new task card, which changes the project it goes to. It is
|
|
73
|
+
// gated to that one card, the card draws its projects where she can see
|
|
74
|
+
// them, and it used to ride along inside the C row's sentence, which is the
|
|
75
|
+
// fault the NO KEY HIDES INSIDE A SENTENCE block above is about.
|
|
76
|
+
//
|
|
77
|
+
// ⇧⇥ in the reply box, which cycles Claude Code's permission modes. Real,
|
|
78
|
+
// and it belongs to the reply box rather than to the app; it is also the one
|
|
79
|
+
// key here that cannot be said without the word "permissions", and Settings
|
|
80
|
+
// already has a whole group explaining those.
|
|
81
|
+
//
|
|
82
|
+
// ⌘R, ⌘=, ⌘- and ⌘0. The Mac's own chords, answered in main.mjs so they work
|
|
83
|
+
// over the game. Nobody needs to be taught reload and zoom.
|
|
84
|
+
//
|
|
85
|
+
// EVERY SENTENCE IS PLAIN. A tester does not know what a modal, a pane, a
|
|
86
|
+
// binary or a view is. No row uses a word the app does not print on screen
|
|
87
|
+
// somewhere she can see it. THE SIDEBAR ROW IS WHY THAT NOW MATTERS TWICE: it
|
|
88
|
+
// used to name the sections, "Inbox, Scheduled, In progress, Closed", and
|
|
89
|
+
// that went wrong in both halves. The real order is Inbox, In
|
|
90
|
+
// progress, Scheduled, Closed (`workspaceDestinations`), and Scheduled is
|
|
91
|
+
// drawn only when something is actually scheduled. A reference
|
|
92
|
+
// page that recites a list the sidebar can change goes stale without anybody
|
|
93
|
+
// touching it, so the row names no sections at all.
|
|
94
|
+
|
|
95
|
+
import { NAME } from './product-name.mjs';
|
|
96
|
+
|
|
97
|
+
/** @type {import('./shortcuts.d.mts').ShortcutGroup[]} */
|
|
98
|
+
export const SHORTCUTS = [
|
|
99
|
+
{
|
|
100
|
+
// App.tsx, the `switch (e.key)` at the foot of the key handler: this is the
|
|
101
|
+
// list with nothing open, which is where anybody starts.
|
|
102
|
+
//
|
|
103
|
+
// "IN YOUR INBOX", BECAUSE INBOX IS A WORD THE APP PRINTS AND TASK LIST IS
|
|
104
|
+
// NOT (w-1bc916a880, third round).
|
|
105
|
+
//
|
|
106
|
+
// The mistake is worth naming because it is the same one
|
|
107
|
+
// the round before was meant to fix. "Task list" was reached for to cover
|
|
108
|
+
// the fact that these keys work in all four sidebar sections rather than
|
|
109
|
+
// only the first, which is an accurate thing about the code and a phrase
|
|
110
|
+
// nobody sees on a screen. Inbox is the first row of the
|
|
111
|
+
// sidebar. The heading claims these keys work there; it does not claim they
|
|
112
|
+
// work ONLY there, and one of its rows moves between the sections anyway.
|
|
113
|
+
label: 'In your inbox',
|
|
114
|
+
keys: [
|
|
115
|
+
// case 'j': case 'J': case 'ArrowDown' → selected + 1
|
|
116
|
+
{ keys: ['J', '↓'], join: 'or', what: 'Move down the list.' },
|
|
117
|
+
// case 'k': case 'K': case 'ArrowUp' → selected - 1
|
|
118
|
+
{ keys: ['K', '↑'], join: 'or', what: 'Move up the list.' },
|
|
119
|
+
// case 'ArrowLeft': case 'ArrowRight' → boardSideways (w-23fc91bff5).
|
|
120
|
+
// "Board" is the word the View and filters menu prints.
|
|
121
|
+
{ keys: ['←', '→'], what: 'Move to the next column on the board.' },
|
|
122
|
+
// case 'Enter' → setFocused(pointed)
|
|
123
|
+
{ keys: ['↵'], what: 'Open the task you are on.' },
|
|
124
|
+
// App.tsx, `if (e.key === 'Tab')`: nextTab(stateTabOrder, view, shiftKey)
|
|
125
|
+
// walks the tabs along the top of the inbox, wrapping. It replaced ⌘1 to
|
|
126
|
+
// ⌘4 on 2026-10-02 (w-914b16eab6), which jumped through the sidebar's
|
|
127
|
+
// sections before those became these tabs. Shift-Tab goes back and is
|
|
128
|
+
// left unlisted on her word: "just show the Next tab".
|
|
129
|
+
{ keys: ['⇥'], what: 'Move to the next tab along the top.' },
|
|
130
|
+
// case 'b': case 'B' → setInboxDisplay(flipView(inboxDisplay)), asked
|
|
131
|
+
// for by name (w-58c8f466e7). "List" and "Board" are the words the View
|
|
132
|
+
// and filters menu prints, so the sentence uses those.
|
|
133
|
+
{ keys: ['B'], what: 'Switch between the list and the board.' },
|
|
134
|
+
// Escape in the focused branch: doc → task → list. In the list switch it
|
|
135
|
+
// clears a selection and closes search.
|
|
136
|
+
{ keys: ['esc'], what: 'Go back one step: a file, then the task, then the list.' },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
// App.tsx, the `if (focused)` branch: a task open in front of her. NOT
|
|
141
|
+
// "answering an agent" any more, because ⌘J opens its terminal and that is
|
|
142
|
+
// not an answer; the heading says when the keys work, which is the scheme.
|
|
143
|
+
label: 'In a task you have opened',
|
|
144
|
+
keys: [
|
|
145
|
+
// focused branch: 'r' → setModal('reply'); list switch: 'r' in the inbox
|
|
146
|
+
{ keys: ['R'], what: 'Write back to the agent.' },
|
|
147
|
+
// Compose.tsx onKeyDown: (meta||ctrl) && Enter → send; Focus.tsx the same
|
|
148
|
+
{ keys: ['⌘↵'], what: 'Send what you have typed.' },
|
|
149
|
+
// focused branch: ArrowDown / ArrowUp walk opts
|
|
150
|
+
{ keys: ['↑', '↓'], what: 'Move through the options an agent has offered.' },
|
|
151
|
+
// focused branch: Enter sends optionSel. Its own row rather than a clause
|
|
152
|
+
// at the end of the one above, so somebody scanning for ↵ finds it.
|
|
153
|
+
{ keys: ['↵'], what: 'Send the option you have selected.' },
|
|
154
|
+
// focused branch: /^[1-9]$/ → pickOption(focused, Number(e.key))
|
|
155
|
+
{ keys: ['1', '9'], join: 'to', what: 'Pick an option by its number.' },
|
|
156
|
+
// ⌘J, above the inInput gate in App.tsx: dispatches task-terminal-toggle,
|
|
157
|
+
// and toasts "Open a task to use its terminal" when there is none
|
|
158
|
+
// (w-ef92663cb1). It landed after this list last changed, so it was
|
|
159
|
+
// missing here at first.
|
|
160
|
+
{ keys: ['⌘J'], what: 'Show or hide this task’s terminal.' },
|
|
161
|
+
// threads/Summary.tsx, `useSummaryShortcut`: it listens in the capture
|
|
162
|
+
// phase and stops the key there, so S belongs to the summary on a thread
|
|
163
|
+
// that has one. It is under THIS heading and not the one below because it
|
|
164
|
+
// is the one key in the app that works only inside an open thread.
|
|
165
|
+
{ keys: ['S'], what: 'Show or hide the summary.' },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
// The one group that cannot be named by WHERE she is, because all four of
|
|
170
|
+
// its keys work both in the list and inside an open task. So it is named by
|
|
171
|
+
// what the keys do, literally and in the order they are drawn.
|
|
172
|
+
label: 'Closing, scheduling and undoing',
|
|
173
|
+
keys: [
|
|
174
|
+
// focused branch: 'e' → markDone. List switch: inbox → markDone,
|
|
175
|
+
// snoozed → unsnooze. It never approves anything.
|
|
176
|
+
{ keys: ['E'], what: 'Close the task and take it out of your inbox.' },
|
|
177
|
+
// focused branch and list switch: 'l' → openSnooze. IT WAS S UNTIL
|
|
178
|
+
// 2026-10-01 AND THAT IS THE WHOLE OF WHY IT MOVED. S had come to mean
|
|
179
|
+
// the summary inside a thread and still meant the schedule picker in the
|
|
180
|
+
// list, so this page taught one letter for two different things and an
|
|
181
|
+
// office manager testing the app met both inside a minute. One meaning
|
|
182
|
+
// per letter: S is the summary, L is later.
|
|
183
|
+
{ keys: ['L'], what: 'Put the task off until a time you pick.' },
|
|
184
|
+
// focused branch and list switch: 'z' → undo
|
|
185
|
+
{ keys: ['Z'], what: 'Undo the last thing you did.' },
|
|
186
|
+
// (meta||ctrl) && 'a' && !inInput && !modal → tick the whole list
|
|
187
|
+
{ keys: ['⌘A'], what: 'Tick every task in the list. Press it again to untick.' },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
label: 'Anywhere in the app',
|
|
192
|
+
keys: [
|
|
193
|
+
// (meta||ctrl) && 'k' → the palette, above every other branch, and
|
|
194
|
+
// forwarded from inside an open file by main.mjs as well.
|
|
195
|
+
{ keys: ['⌘K'], what: `The command bar: everything ${NAME} can do, by name.` },
|
|
196
|
+
// 'n' in both branches → setModal('compose'). C does the same and is
|
|
197
|
+
// left off: N is the default (w-fb9051e597).
|
|
198
|
+
// "Threads", not tasks, since w-ec62ab6b38.
|
|
199
|
+
{ keys: ['N'], what: 'Start a new thread.' },
|
|
200
|
+
// '/' above the focused branch → openSearch
|
|
201
|
+
{ keys: ['/'], what: 'Search your threads.' },
|
|
202
|
+
// '\\' above the focused branch → togglePanel
|
|
203
|
+
{ keys: ['\\'], what: 'Show or hide the left sidebar.' },
|
|
204
|
+
// main/main.mjs before-input-event: ⌘Y / ⌘N answer the oldest approval.
|
|
205
|
+
// Its own card prints "⌘Y / ⌘N" beside Allow and Deny, so the words here
|
|
206
|
+
// are the card's words. It lives under this heading rather than under an
|
|
207
|
+
// open task because it is answered from anywhere at all.
|
|
208
|
+
{ keys: ['⌘Y', '⌘N'], join: 'or', what: 'Allow or deny a card asking to run something.' },
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/** Every cap drawn on the page, flattened. The test walks this. */
|
|
214
|
+
export function everyKey(groups = SHORTCUTS) {
|
|
215
|
+
return groups.flatMap((g) => g.keys.flatMap((k) => k.keys));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// WHAT AN AGENT IS TOLD, and it is this page and nothing else, so a key that
|
|
219
|
+
// moves here has moved for every agent on its next run. The closing line is
|
|
220
|
+
// the point: an agent's own saved memory is read before anything we hand it,
|
|
221
|
+
// and on 2026-10-04 one said "press S" three days after S stopped scheduling.
|
|
222
|
+
/** The keys as one block of plain text, for an agent's system prompt. */
|
|
223
|
+
export function keysForAgents(groups = SHORTCUTS) {
|
|
224
|
+
const lines = [
|
|
225
|
+
`The keys in ${NAME}, the app this person reads you in, as they are today.`,
|
|
226
|
+
'When you tell them to press something, use these. Keys have moved before,',
|
|
227
|
+
'so a key you remember from an earlier session, a memory or a note that',
|
|
228
|
+
'disagrees with this list is out of date. Anything without a key is in ⌘K',
|
|
229
|
+
'by name.',
|
|
230
|
+
];
|
|
231
|
+
for (const g of groups) {
|
|
232
|
+
lines.push('', `${g.label}:`);
|
|
233
|
+
for (const k of g.keys) lines.push(`- ${k.keys.join(k.join ? ` ${k.join} ` : ' ')}: ${k.what}`);
|
|
234
|
+
}
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
@@ -6,3 +6,5 @@
|
|
|
6
6
|
// main and by the tests, which are both plain JS and need nothing here.
|
|
7
7
|
|
|
8
8
|
export function strandedTitle(count: number): string;
|
|
9
|
+
/** The line in a thread where a run stopped, in our words; `raw` is the tool's own refusal. */
|
|
10
|
+
export function runStoppedLine(raw: string): string;
|
package/shared/spawn-trouble.mjs
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
// across the top of her inbox. Her three complaints were all correct. There is
|
|
7
7
|
// no OAuth in Agentbox, so the sentence names a thing that does not exist here.
|
|
8
8
|
// It is ugly. And it was on screen while agents were working perfectly well,
|
|
9
|
-
// because it was about one
|
|
9
|
+
// because it was about one Claude account among several and not about the app.
|
|
10
10
|
//
|
|
11
11
|
// So the raw text never reaches the renderer again. It goes in the session's
|
|
12
12
|
// own trace log, which is where a person debugging wants it, and what she is
|
|
13
|
-
// shown is one sentence of ours written in
|
|
13
|
+
// shown is one sentence of ours written in plain words.
|
|
14
14
|
//
|
|
15
15
|
// Every sentence here has to survive the same test: read it out loud to
|
|
16
16
|
// somebody who has never heard of a subprocess, and they know what happened
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// CODEX. It is right about Codex anyway, and that is measured rather than
|
|
25
25
|
// assumed. On 2026-09-05 `codex app-server` (codex-cli 0.148.0) was driven the
|
|
26
26
|
// way main/codex-app-server.mjs drives it, with CODEX_HOME pointed at an empty
|
|
27
|
-
// directory so there was no auth.json and
|
|
27
|
+
// directory so there was no auth.json and the real ~/.codex login was neither
|
|
28
28
|
// read nor written. The turn retried ten times and failed with
|
|
29
29
|
//
|
|
30
30
|
// unexpected status 401 Unauthorized: Missing bearer or basic authentication in
|
|
@@ -41,18 +41,18 @@
|
|
|
41
41
|
|
|
42
42
|
// AN ACCOUNT THAT IS SIGNED IN AND NOT ALLOWED TO RUN.
|
|
43
43
|
//
|
|
44
|
-
//
|
|
45
|
-
// time
|
|
46
|
-
// off. Run by hand
|
|
44
|
+
// It happens. A login can be intact, with the browser sign-in succeeding every
|
|
45
|
+
// time, while the WORKSPACE that account belongs to has Claude Code turned
|
|
46
|
+
// off. Run by hand, this is the whole of what the CLI says:
|
|
47
47
|
//
|
|
48
48
|
// Your organization has disabled Claude subscription access for Claude Code ·
|
|
49
49
|
// Use an Anthropic API key instead, or ask your admin to enable access
|
|
50
50
|
//
|
|
51
51
|
// "subscription access" is in the list below, so every one of those was filed
|
|
52
|
-
// as a login that had run out and
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
52
|
+
// as a login that had run out and the person was told to type /login. Doing
|
|
53
|
+
// it again and again changes nothing, because no login can change an admin's
|
|
54
|
+
// policy, and every dead run on the row carried "Sign that account back in
|
|
55
|
+
// and it picks straight up."
|
|
56
56
|
//
|
|
57
57
|
// TESTED BEFORE SIGNED_OUT, which is the whole of the fix: these lines say
|
|
58
58
|
// "subscription" and "access" and mean something else entirely.
|
|
@@ -188,9 +188,8 @@ export function troubleCause(raw) {
|
|
|
188
188
|
//
|
|
189
189
|
// carries the one thing she actually wants to know, and we were throwing it
|
|
190
190
|
// away and then telling her the vaguer "when the limit resets". The timezone
|
|
191
|
-
// is dropped rather than shown:
|
|
192
|
-
//
|
|
193
|
-
// add a word she has to read. Null when there is no time to quote, and then
|
|
191
|
+
// is dropped rather than shown: the limit line prints the zone the Mac is
|
|
192
|
+
// already set to, so naming it would only add a word she has to read. Null when there is no time to quote, and then
|
|
194
193
|
// the sentence falls back to the wording that has always been there.
|
|
195
194
|
export function limitResetsAt(raw) {
|
|
196
195
|
// Both wordings are real: the CLI's own line is "resets 6pm" and the API's
|
|
@@ -202,14 +201,14 @@ export function limitResetsAt(raw) {
|
|
|
202
201
|
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
203
202
|
|
|
204
203
|
// THE SAME HOUR AS A MOMENT, so the fleet can wait exactly until it
|
|
205
|
-
// (
|
|
204
|
+
// (a limit that reset at, say, 6:50pm could hold every agent until 7:03pm,
|
|
206
205
|
// because the app waited a fixed thirty minutes instead of reading this line).
|
|
207
206
|
// The next local occurrence of that clock time at or after `from`; null when
|
|
208
207
|
// the line names no hour.
|
|
209
208
|
export function limitResetMoment(raw, from = Date.now()) {
|
|
210
209
|
// CODEX NAMES A DAY, NOT AN HOUR: "try again at Sep 28th, 2026 1:05 PM"
|
|
211
210
|
// (codex-cli 0.153.4, 2026-09-24). A weekly limit read as "thirty minutes"
|
|
212
|
-
// put the capped login back in the pool every half hour for
|
|
211
|
+
// put the capped login back in the pool every half hour for days.
|
|
213
212
|
const dated = /try again at ([a-z]{3,9})\.? (\d{1,2})(?:st|nd|rd|th)?,? (\d{4}),? (\d{1,2}):(\d{2})\s*(am|pm)/i.exec(String(raw ?? ''));
|
|
214
213
|
if (dated) {
|
|
215
214
|
const month = MONTHS.indexOf(dated[1].slice(0, 3).toLowerCase());
|
|
@@ -343,8 +342,8 @@ export function engineTroubleNote(cause, engineWord = 'Codex') {
|
|
|
343
342
|
// The count is the point, and it is why this is a different sentence from
|
|
344
343
|
// `troubleSentence` above rather than a rewording of it. That one is about the
|
|
345
344
|
// FLEET, and it can only ever be true when every account on the Mac is down at
|
|
346
|
-
// once. That is why
|
|
347
|
-
// signed-out login
|
|
345
|
+
// once. That is why the screen could stay quiet: Codex tasks and tasks on a
|
|
346
|
+
// signed-out login could sit there all day while the other agents worked, so the
|
|
348
347
|
// fleet was never down and the one line the app owns was structurally unable to
|
|
349
348
|
// speak. The subject here is her TASKS, which is the thing she went looking for
|
|
350
349
|
// and the thing that was actually stuck.
|
|
@@ -443,6 +442,28 @@ const DEAD_RUN_OPENERS = {
|
|
|
443
442
|
unknown: 'stopped before it did anything, so this task has not been started.',
|
|
444
443
|
};
|
|
445
444
|
|
|
445
|
+
/**
|
|
446
|
+
* THE LINE IN A THREAD WHERE A RUN STOPPED, in our words.
|
|
447
|
+
*
|
|
448
|
+
* The CLI types its own refusal into the run as if the agent had said it
|
|
449
|
+
* ("Failed to authenticate: OAuth session expired..."), and the thread drew it
|
|
450
|
+
* as the agent's message. This is about one run, not the task: a run that
|
|
451
|
+
* stops mid-thread has usually done plenty before it, so it says nothing about
|
|
452
|
+
* what has or has not been done.
|
|
453
|
+
*/
|
|
454
|
+
const RUN_STOPPED = {
|
|
455
|
+
'signed-out': 'This run stopped because the account was signed out.',
|
|
456
|
+
'org-blocked': 'This run stopped because that account is switched off by its organization.',
|
|
457
|
+
'at-limit': 'This run stopped because the account hit its usage limit.',
|
|
458
|
+
workspace: "This run stopped because it would not start in this project's folder.",
|
|
459
|
+
interrupted: 'This run was cut off in the middle.',
|
|
460
|
+
unknown: 'This run stopped before it finished.',
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
export function runStoppedLine(raw) {
|
|
464
|
+
return RUN_STOPPED[troubleCause(raw)] ?? RUN_STOPPED.unknown;
|
|
465
|
+
}
|
|
466
|
+
|
|
446
467
|
export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', runs = 1, resetsAt = null } = {}) {
|
|
447
468
|
const opener = `${engineWord} ${DEAD_RUN_OPENERS[cause] ?? DEAD_RUN_OPENERS.unknown}`;
|
|
448
469
|
|
|
@@ -468,7 +489,7 @@ export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', r
|
|
|
468
489
|
// WHAT GOES ON HER ROW WHEN THE RUN WAS FINE AND HER ANSWER STILL WENT NOWHERE.
|
|
469
490
|
//
|
|
470
491
|
// `settleDelivery` is two independent tests joined with an AND: did the process
|
|
471
|
-
// exit cleanly, and did an agent leave a word on the row since
|
|
492
|
+
// exit cleanly, and did an agent leave a word on the row since the user wrote. A run
|
|
472
493
|
// can pass the first and fail the second, and several ordinary ones do -- a
|
|
473
494
|
// Codex turn that talks in its thread and never calls a store tool, a worker
|
|
474
495
|
// that claims a row and finishes without writing, any session on an install
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Types for shared/team-rules.mjs, read by the renderer.
|
|
2
|
+
type Row = { createdBy?: string | null; assignee?: string; runner?: string; people?: string[]; status?: string };
|
|
3
|
+
type ProductLike = { team?: { projectId?: string; sharedBy?: string | null } | null } | null | undefined;
|
|
4
|
+
|
|
5
|
+
export function isShared(product: ProductLike): boolean;
|
|
6
|
+
export function runnerOf(item: Row, product: ProductLike): string | null;
|
|
7
|
+
export function heldByAPerson(item: Row): boolean;
|
|
8
|
+
export function mayRunHere(item: Row, product: ProductLike, me: string | null): boolean;
|
|
9
|
+
export function inMyInbox(item: Row, product: ProductLike, me: string | null): boolean;
|
|
10
|
+
export function lastSpeaker(item: Row): string | null;
|
|
11
|
+
export function iSpokeLast(item: Row, product: ProductLike, me: string | null): boolean;
|
|
12
|
+
export function handedOnByReply(item: Row, product: ProductLike, me: string | null): string | null;
|
|
13
|
+
/** One thread you are in where somebody spoke after you (w-920461cbe6). */
|
|
14
|
+
export interface ThreadWaiting { uid: string; mine: boolean; fresh: number; people: string[]; text: string; last: string; lastBy: string }
|
|
15
|
+
/** What in a conversation waits on you; null on a row from before threads. */
|
|
16
|
+
export interface Waits { chat: number; threads: ThreadWaiting[] }
|
|
17
|
+
export function whatWaits(item: Row & { talk?: unknown }, me: string | null): Waits | null;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// THE TEAM'S RULES ABOUT ROWS, IN ONE PLACE. Pure and browser-safe: the
|
|
2
|
+
// supervisor asks them before it runs anything, and the window asks them
|
|
3
|
+
// before it puts a row in somebody's inbox, so the two can never disagree about
|
|
4
|
+
// whose a row is.
|
|
5
|
+
//
|
|
6
|
+
// A project is shared when its record carries a team id (main/team/projects.mjs).
|
|
7
|
+
// Everything here leaves a private project exactly as it always was.
|
|
8
|
+
import { pickFields } from './work-items.mjs';
|
|
9
|
+
|
|
10
|
+
export const isShared = (product) => !!product?.team?.projectId;
|
|
11
|
+
|
|
12
|
+
// THE PERSON WHOSE MAC RUNS AGENTS ON A ROW: whoever it was handed to, else
|
|
13
|
+
// whoever started it, else (a row from before anyone signed in) whoever shared
|
|
14
|
+
// the project.
|
|
15
|
+
export function runnerOf(item, product) {
|
|
16
|
+
return item?.runner || item?.createdBy || product?.team?.sharedBy || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A row a person has been given waits for that person, not for an agent.
|
|
20
|
+
export const heldByAPerson = (item) => !!item?.assignee && item.assignee !== 'agent';
|
|
21
|
+
|
|
22
|
+
// May a worker run on this row on this Mac, signed in as `me`?
|
|
23
|
+
export function mayRunHere(item, product, me) {
|
|
24
|
+
if (!isShared(product)) return true;
|
|
25
|
+
if (!me) return false;
|
|
26
|
+
// A MESSAGE BETWEEN TWO PEOPLE NEVER RUNS AN AGENT, on either Mac. Turning
|
|
27
|
+
// one into work is @-mentioning an agent in the reply box, which starts a new
|
|
28
|
+
// thread of your own in the project the mention names; nothing a teammate
|
|
29
|
+
// writes into the shared record can start one here.
|
|
30
|
+
if (product?.team?.direct) return false;
|
|
31
|
+
if (heldByAPerson(item)) return false;
|
|
32
|
+
// ONLY YOU PUT YOUR MAC IN CHARGE OF A ROW. A runner naming you that somebody
|
|
33
|
+
// else's line wrote is a teammate trying to start an agent on your Mac with
|
|
34
|
+
// their words, so it does not count.
|
|
35
|
+
const runnerBy = item?.wrote?.runner?.by;
|
|
36
|
+
if (item?.runner === me && runnerBy && runnerBy !== me) return false;
|
|
37
|
+
// NOR DOES A TEAMMATE'S WORD START ONE. Lines pulled from teammates may not
|
|
38
|
+
// set these fields at all (whatATeammateMaySet below); this is the second
|
|
39
|
+
// wall, for a ledger that already holds such a line from before that rule.
|
|
40
|
+
// A field with no writer is from before anyone signed in, and is yours.
|
|
41
|
+
for (const field of STARTS_A_RUN) {
|
|
42
|
+
const by = item?.wrote?.[field]?.by;
|
|
43
|
+
if (by && by !== me) return false;
|
|
44
|
+
}
|
|
45
|
+
return runnerOf(item, product) === me;
|
|
46
|
+
}
|
|
47
|
+
const STARTS_A_RUN = ['body', 'answer', 'status', 'runAt'];
|
|
48
|
+
|
|
49
|
+
// WHOSE INBOX A SHARED ROW IS IN: one person's at a time, whoever has to act.
|
|
50
|
+
// A row given to a person is theirs until it is done; an agent's row is its
|
|
51
|
+
// runner's. True leaves the row to the inbox's ordinary rules, which is what a
|
|
52
|
+
// private row, or any row on a Mac nobody is signed in on, always gets.
|
|
53
|
+
// WHO SPOKE LAST IN A CONVERSATION: the writer of its newest message.
|
|
54
|
+
export function lastSpeaker(item) {
|
|
55
|
+
const a = item?.wrote?.answer;
|
|
56
|
+
const b = item?.wrote?.body;
|
|
57
|
+
const latest = a && (!b || (a.ts ?? 0) >= (b.ts ?? 0)) ? a : b;
|
|
58
|
+
return latest?.by ?? item?.createdBy ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* WHAT IN A CONVERSATION IS WAITING ON YOU (w-920461cbe6). Asked the day
|
|
63
|
+
* threads shipped: in a busy chat, with three people answering your thread and
|
|
64
|
+
* others carrying on in the chat, what reaches your inbox? Not a row per
|
|
65
|
+
* message, and not a thread you are not in; and a thread waiting on you must
|
|
66
|
+
* not be lost because you said something in the chat.
|
|
67
|
+
*
|
|
68
|
+
* So: `chat` is how many messages in the main chat came after your own newest
|
|
69
|
+
* one there, and `threads` is every thread you are IN (you wrote the message it
|
|
70
|
+
* hangs off, or replied in it) where somebody else spoke after you, newest
|
|
71
|
+
* first, with how many replies are new, who wrote them, what the thread is
|
|
72
|
+
* about and its newest words. Null for a row with no record of who spoke where
|
|
73
|
+
* (one from before threads), which then reads by `lastSpeaker` as it always did.
|
|
74
|
+
*/
|
|
75
|
+
export function whatWaits(item, me) {
|
|
76
|
+
const talk = item?.talk;
|
|
77
|
+
if (!talk || !me) return null;
|
|
78
|
+
const since = (list) => { let n = 0; for (let k = list.length - 1; k >= 0 && list[k].by !== me; k -= 1) n += 1; return n; };
|
|
79
|
+
const threads = [];
|
|
80
|
+
for (const [uid, t] of Object.entries(talk.threads ?? {})) {
|
|
81
|
+
const replies = t.replies ?? [];
|
|
82
|
+
if (t.by !== me && !replies.some((r) => r.by === me)) continue;
|
|
83
|
+
const fresh = since(replies);
|
|
84
|
+
if (!fresh) continue;
|
|
85
|
+
const news = replies.slice(-fresh);
|
|
86
|
+
threads.push({
|
|
87
|
+
uid, mine: t.by === me, fresh,
|
|
88
|
+
people: [...new Set(news.map((r) => r.by))],
|
|
89
|
+
text: t.text, last: news[news.length - 1].text, lastBy: news[news.length - 1].by,
|
|
90
|
+
at: news[news.length - 1].ts,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
threads.sort((a, b) => b.at - a.at);
|
|
94
|
+
return { chat: since(talk.chat ?? []), threads: threads.map(({ at, ...t }) => t) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function inMyInbox(item, product, me) {
|
|
98
|
+
if (!isShared(product) || !me) return true;
|
|
99
|
+
// A CONVERSATION NEEDS WHOEVER DID NOT SPEAK LAST (2026-10-01), so a message
|
|
100
|
+
// to three people is in all three inboxes until one of them answers, and the
|
|
101
|
+
// answer puts it back in everyone else's. One person to one works the same.
|
|
102
|
+
// WITH THREADS (w-920461cbe6) the same rule is read per place: the chat, and
|
|
103
|
+
// each thread you are in. One row, whichever of them is waiting.
|
|
104
|
+
if (product?.team?.direct) {
|
|
105
|
+
if (item.status === 'done') return false;
|
|
106
|
+
const waits = whatWaits(item, me);
|
|
107
|
+
if (waits) return waits.chat > 0 || waits.threads.length > 0;
|
|
108
|
+
const by = lastSpeaker(item);
|
|
109
|
+
return by ? by !== me : item.assignee === me;
|
|
110
|
+
}
|
|
111
|
+
if (heldByAPerson(item)) return item.assignee === me && item.status !== 'done';
|
|
112
|
+
return runnerOf(item, product) === me;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A CONVERSATION WHERE YOU SPOKE LAST IS DONE FOR YOU (w-57a202a968), until
|
|
116
|
+
// the other person writes again and `inMyInbox` brings it back. Without it a
|
|
117
|
+
// message you answered was on no tab and no column: out of your inbox by the
|
|
118
|
+
// rule above, never In progress, and not Done because its status is not done.
|
|
119
|
+
// Her words: "It's not supposed to leave the board; it's supposed to go in Done."
|
|
120
|
+
export function iSpokeLast(item, product, me) {
|
|
121
|
+
if (!me || !product?.team?.direct || item?.status === 'done') return false;
|
|
122
|
+
// Done for you means nothing in it waits on you: the chat or any thread.
|
|
123
|
+
const waits = whatWaits(item, me);
|
|
124
|
+
if (waits) return waits.chat === 0 && waits.threads.length === 0;
|
|
125
|
+
return lastSpeaker(item) === me;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// A REPLY HANDS A PERSON-TO-PERSON ROW TO THE OTHER PERSON, so a conversation
|
|
129
|
+
// goes back and forth between two inboxes instead of sitting in both. Returns
|
|
130
|
+
// who it goes to, or null when a reply changes nothing about whose it is.
|
|
131
|
+
export function handedOnByReply(item, product, me) {
|
|
132
|
+
if (!isShared(product) || !me || !heldByAPerson(item) || item.assignee !== me) return null;
|
|
133
|
+
const people = Array.isArray(item.people) ? item.people : [];
|
|
134
|
+
return people.find((p) => p !== me) || (item.createdBy && item.createdBy !== me ? item.createdBy : null);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// WHAT A TEAMMATE'S LINE MAY SET ON THIS MAC. Found by review 2026-10-01: a
|
|
138
|
+
// teammate's line with source "founder" could set the body, the answer, the
|
|
139
|
+
// status, the schedule or the model of any row in a shared project, including
|
|
140
|
+
// a row whose runner is you, and the supervisor then started an agent here on
|
|
141
|
+
// their words. So a pulled line keeps only the summary (which the board shows
|
|
142
|
+
// anyway), who has to act next, and, in a message between two people (which
|
|
143
|
+
// never runs an agent), the message itself. Everything else is dropped before
|
|
144
|
+
// it reaches the disk, claims and epochs included. Returns null when nothing
|
|
145
|
+
// is left.
|
|
146
|
+
const A_TEAMMATE_MAY_SET = ['problem', 'progress', 'solution', 'blockedBy', 'blocks', 'assignee'];
|
|
147
|
+
// The status rides too: a new message reopens a conversation put away, and a
|
|
148
|
+
// message record never starts an agent (mayRunHere).
|
|
149
|
+
//
|
|
150
|
+
// AND A REACTION, which is the chips under a message (w-560647d4db). It is on
|
|
151
|
+
// this list rather than the one above because it is only ever drawn in a
|
|
152
|
+
// conversation, and because an unrecognised delta on an ordinary shared task
|
|
153
|
+
// would be a teammate writing a field nothing there reads. It cannot start an
|
|
154
|
+
// agent: `STARTS_A_RUN` above names the four fields that can, and a reaction
|
|
155
|
+
// touches none of them, so a chip never puts the row in anybody's inbox.
|
|
156
|
+
//
|
|
157
|
+
// AND WHICH MESSAGE A REPLY ANSWERS (w-920461cbe6), for the same two reasons:
|
|
158
|
+
// only a conversation draws a thread, and the mark rides beside an `answer`
|
|
159
|
+
// this list already lets through. Without it a teammate's thread reply would
|
|
160
|
+
// land in the middle of your chat instead of in the thread.
|
|
161
|
+
const IN_A_MESSAGE_ALSO = ['title', 'body', 'answer', 'people', 'status', 'react', 'inReplyTo'];
|
|
162
|
+
// AND THE LEVEL THE SENDER PICKED, ON THE OPENING LINE ALONE (w-7ba439c883).
|
|
163
|
+
// A message now carries how urgent its sender thought it was, and that number
|
|
164
|
+
// decides where it sits in the inbox it lands in, so it has to survive the
|
|
165
|
+
// pull. It may not become a way to re-rank a row afterwards, though: the level
|
|
166
|
+
// rides the line that MAKES the row and no other.
|
|
167
|
+
//
|
|
168
|
+
// `kind` is how that line is known. Only a row's first line sets it, and a
|
|
169
|
+
// teammate may not set it here at all, so it is a marker rather than a field:
|
|
170
|
+
// it is read, then dropped with everything else that is not allowed.
|
|
171
|
+
//
|
|
172
|
+
// WHAT THIS DOES NOT STOP, said plainly: a teammate who forges a line carrying
|
|
173
|
+
// a kind can still re-rank the one conversation the two of you share. They can
|
|
174
|
+
// already rewrite that row's title, body, answer and status (above), so this
|
|
175
|
+
// opens no row that was closed to them, and nothing outside a conversation is
|
|
176
|
+
// reachable either way.
|
|
177
|
+
const WHEN_A_MESSAGE_OPENS = ['priority'];
|
|
178
|
+
const opensARow = (patch) => 'kind' in patch;
|
|
179
|
+
export function whatATeammateMaySet(line, { direct = false } = {}) {
|
|
180
|
+
if (!line || typeof line !== 'object' || !line.patch || typeof line.patch !== 'object') return null;
|
|
181
|
+
const allowed = direct
|
|
182
|
+
? [...A_TEAMMATE_MAY_SET, ...IN_A_MESSAGE_ALSO, ...(opensARow(line.patch) ? WHEN_A_MESSAGE_OPENS : [])]
|
|
183
|
+
: A_TEAMMATE_MAY_SET;
|
|
184
|
+
const kept = {};
|
|
185
|
+
for (const field of allowed) if (field in line.patch) kept[field] = line.patch[field];
|
|
186
|
+
const patch = pickFields(kept);
|
|
187
|
+
if (!patch) return null;
|
|
188
|
+
return { id: line.id, ts: line.ts, source: line.source, by: line.by, uid: line.uid, patch };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// A LEASE BELONGS TO THE MAC THAT TOOK IT. A claim, an epoch, a release or a
|
|
192
|
+
// heartbeat is a worker's hold on one Mac, so it never rides a line across to
|
|
193
|
+
// another one — not even to a second copy of the app that is also yours.
|
|
194
|
+
export function withoutALease(line) {
|
|
195
|
+
const out = { ...line };
|
|
196
|
+
delete out.claim; delete out.epoch; delete out.release; delete out.heartbeat;
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// A LINE AS IT COMES BACK FROM THE CLOUD, the same on both backends. Who wrote
|
|
201
|
+
// it and when are the database's (by_person, created_at), never the copy in
|
|
202
|
+
// the body, and a teammate's line carries no lease (above).
|
|
203
|
+
export function asPulled(body, { by, at, me }) {
|
|
204
|
+
const line = { ...body, by };
|
|
205
|
+
if (by === me) return line;
|
|
206
|
+
const kept = withoutALease(line);
|
|
207
|
+
if (Number.isFinite(at)) kept.ts = at;
|
|
208
|
+
return kept;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// YOUR OWN LINE, COMING BACK FROM A SECOND COPY OF THE APP SIGNED IN AS YOU.
|
|
212
|
+
//
|
|
213
|
+
// Reported 2026-10-04 (w-2fce569057): a message sent to a teammate never came
|
|
214
|
+
// back. It had been sent from a second copy. It reached the cloud and it
|
|
215
|
+
// reached the teammate's app; what it never reached was the everyday copy,
|
|
216
|
+
// because the pull skipped every line whose writer was you, on the reasoning
|
|
217
|
+
// that a line you wrote is already here. True of the copy that wrote it, false
|
|
218
|
+
// of every other copy you own, and a conversation missing your own half reads
|
|
219
|
+
// as a message that never sent.
|
|
220
|
+
//
|
|
221
|
+
// ONLY IN A MESSAGE RECORD, where no agent ever runs (mayRunHere). On an
|
|
222
|
+
// ordinary shared row your own answer, body or status is exactly what starts
|
|
223
|
+
// one, and `mayRunHere` believes a line whose writer is you, because it is you:
|
|
224
|
+
// carrying those across would start a second agent on the same row on your
|
|
225
|
+
// other Mac. So nothing crosses there, and your own words stay where you typed
|
|
226
|
+
// them. Returns null when the line is not yours to bring back.
|
|
227
|
+
export function whatYourOtherCopyMaySet(line, { direct = false } = {}) {
|
|
228
|
+
if (!direct || !line || typeof line !== 'object') return null;
|
|
229
|
+
return withoutALease(line);
|
|
230
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface PersonStatus { text: string; until: number | null }
|
|
2
|
+
export type HoldId = 'today' | 'tomorrow' | 'week' | 'open';
|
|
3
|
+
|
|
4
|
+
export const STATUS_MAX: number;
|
|
5
|
+
export const HOLDS: { id: HoldId; label: string }[];
|
|
6
|
+
|
|
7
|
+
export function cleanStatus(text: unknown): string | null;
|
|
8
|
+
export function liveStatus(status: PersonStatus | null | undefined, now?: number): PersonStatus | null;
|
|
9
|
+
export function hasLapsed(status: PersonStatus | null | undefined, now?: number): boolean;
|
|
10
|
+
export function holdsUntil(until: number | null | undefined, now?: number): string;
|
|
11
|
+
export function holdEnds(id: HoldId | string, now?: number): number | null;
|