agentbox-app 0.1.7 → 0.1.12
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 +220 -58
- package/bin/agentbox.mjs +18 -3
- package/briefs/message-rules.md +24 -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 +11 -11
- 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 +116 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/approvals.mjs +14 -5
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +32 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +25 -13
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +18 -0
- package/main/codex-launch-env.mjs +31 -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 +82 -17
- package/main/demo.mjs +17 -14
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +204 -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 +7 -4
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +9 -3
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +134 -60
- package/main/ipc.mjs +335 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +48 -1
- package/main/main.mjs +138 -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/notify.mjs +1 -1
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +21 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +115 -51
- package/main/runs-on.mjs +93 -0
- package/main/serve.mjs +77 -5
- package/main/settings.mjs +245 -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 +84 -7
- package/main/store/project.mjs +15 -19
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +245 -13
- package/main/supervisor.mjs +1634 -417
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +94 -16
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +14 -2
- 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 +10 -12
- package/mcp/core/work.mjs +123 -26
- package/mcp/tools.mjs +13 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
- package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/after-install.mjs +14 -0
- package/scripts/approval-server.sh +7 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +39 -16
- 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 +38 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +6 -6
- 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-modes.mjs +1 -1
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +6 -6
- 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 +34 -4
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +9 -3
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/runs-on.d.mts +20 -0
- package/shared/runs-on.mjs +79 -0
- 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 +40 -19
- 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 +258 -28
- 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,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;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// A STATUS LINE YOU WRITE YOURSELF: the rules, in one place.
|
|
2
|
+
//
|
|
3
|
+
// Four of six people interviewed asked for the same thing, and the founder
|
|
4
|
+
// put it plainest: "If I'm going to spend a day or a few days in meetings, I
|
|
5
|
+
// don't want the board to make it seem like I'm not doing any work." So a
|
|
6
|
+
// person carries one short line they wrote, and it holds until a time they
|
|
7
|
+
// chose.
|
|
8
|
+
//
|
|
9
|
+
// THE EXPIRY IS READ, NOT SWEPT. An expired line is nothing to everyone who
|
|
10
|
+
// reads it, at the moment it expires, without anyone's Mac having to be awake
|
|
11
|
+
// to clear it. The owner's Mac also clears it in the cloud on the next sync
|
|
12
|
+
// after it lapses, so the text does not sit there for good; that is tidying,
|
|
13
|
+
// not what makes it disappear.
|
|
14
|
+
|
|
15
|
+
/** A line is one line. Longer than this is a note, and the places it is drawn
|
|
16
|
+
* (a sidebar row, a hover card) cannot hold one. */
|
|
17
|
+
export const STATUS_MAX = 80;
|
|
18
|
+
|
|
19
|
+
/** What the app stores from what somebody typed: trimmed, cut to one line and
|
|
20
|
+
* to STATUS_MAX, or null when they said nothing. */
|
|
21
|
+
export function cleanStatus(text) {
|
|
22
|
+
const one = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
23
|
+
return one ? one.slice(0, STATUS_MAX) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The status as it reads NOW: null when there is no text, or when the time
|
|
27
|
+
* they chose has passed. Everything that draws a status goes through here. */
|
|
28
|
+
export function liveStatus(status, now = Date.now()) {
|
|
29
|
+
const text = cleanStatus(status?.text);
|
|
30
|
+
if (!text) return null;
|
|
31
|
+
const until = status?.until ?? null;
|
|
32
|
+
if (until && until <= now) return null;
|
|
33
|
+
return { text, until };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when there is text in the cloud that no longer reads, which is the
|
|
37
|
+
* owner's Mac's cue to clear it. */
|
|
38
|
+
export function hasLapsed(status, now = Date.now()) {
|
|
39
|
+
return !!cleanStatus(status?.text) && !!status?.until && status.until <= now;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** How long it holds, in words, for the end of a line: "until 6pm today",
|
|
43
|
+
* "until Monday", "until Mar 3". Empty when it holds until they clear it. */
|
|
44
|
+
export function holdsUntil(until, now = Date.now()) {
|
|
45
|
+
if (!until || until <= now) return '';
|
|
46
|
+
const d = new Date(until);
|
|
47
|
+
if (new Date(now).toDateString() === d.toDateString()) {
|
|
48
|
+
const h = d.getHours();
|
|
49
|
+
const mins = d.getMinutes() ? `:${String(d.getMinutes()).padStart(2, '0')}` : '';
|
|
50
|
+
return `until ${h % 12 === 0 ? 12 : h % 12}${mins}${h < 12 ? 'am' : 'pm'} today`;
|
|
51
|
+
}
|
|
52
|
+
// Inside a week a weekday names itself. Eight days out, "until Monday"
|
|
53
|
+
// would be the Monday after the one they meant, so the date is safer.
|
|
54
|
+
if (until - now < 7 * 86_400_000) return `until ${d.toLocaleDateString(undefined, { weekday: 'long' })}`;
|
|
55
|
+
return `until ${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The four holds the app offers, in the order they are drawn. */
|
|
59
|
+
export const HOLDS = [
|
|
60
|
+
{ id: 'today', label: 'Today' },
|
|
61
|
+
{ id: 'tomorrow', label: 'Tomorrow' },
|
|
62
|
+
{ id: 'week', label: 'This week' },
|
|
63
|
+
{ id: 'open', label: 'Until I clear it' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** When a status set now runs out. A moment they chose wins while it is still
|
|
67
|
+
* ahead ("they should be able to choose their own timelines", 2026-10-04);
|
|
68
|
+
* anything else, now, the past or not a number at all, falls back to the
|
|
69
|
+
* named hold, so a stale or garbled moment can never end a status at once. */
|
|
70
|
+
export function statusEnds({ hold = 'open', until } = {}, now = Date.now()) {
|
|
71
|
+
if (typeof until === 'number' && Number.isFinite(until) && until > now) return until;
|
|
72
|
+
return holdEnds(hold, now);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** When a hold runs out. Today and tomorrow end at the end of that day, so a
|
|
76
|
+
* line set at 9am and one set at 4pm both last the day out. */
|
|
77
|
+
export function holdEnds(id, now = Date.now()) {
|
|
78
|
+
if (id === 'open') return null;
|
|
79
|
+
const d = new Date(now);
|
|
80
|
+
d.setHours(23, 59, 59, 999);
|
|
81
|
+
if (id === 'tomorrow') d.setDate(d.getDate() + 1);
|
|
82
|
+
// THE PARENTHESES ARE LOAD BEARING. Written as `d.getDate() + (7 - day) % 7
|
|
83
|
+
// || 7`, `+` binds tighter than `||`, so on a Sunday it read
|
|
84
|
+
// `(11 + 0) || 7` and set the date to today: "this week" ended tonight.
|
|
85
|
+
if (id === 'week') {
|
|
86
|
+
const toSunday = (7 - d.getDay()) % 7;
|
|
87
|
+
d.setDate(d.getDate() + (toSunday || 7));
|
|
88
|
+
}
|
|
89
|
+
return d.getTime();
|
|
90
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// THE WEB ADDRESSES ON ONE ROW OF THE APP'S TERMINAL, for xterm's link provider
|
|
2
|
+
// (renderer/src/components/TaskTerminal.tsx), which opens them in the browser.
|
|
3
|
+
//
|
|
4
|
+
// The terminal had none, so `claude auth login`'s "If the browser didn't open,
|
|
5
|
+
// visit: https://..." was text you could not click (2026-10-05). That address
|
|
6
|
+
// is about 600 characters and wraps over many rows, so the search runs over the
|
|
7
|
+
// whole LOGICAL line, every row joined to the ones it wraps onto, and a link is
|
|
8
|
+
// handed back for any row it touches, with its range spanning all of them.
|
|
9
|
+
//
|
|
10
|
+
// IT LIVES IN `shared` SO IT CAN BE TESTED without a DOM: xterm's buffer is
|
|
11
|
+
// read through `at(y)`, which answers `{ text, wrapped }` for a 0-based row,
|
|
12
|
+
// `wrapped` being xterm's `isWrapped` (this row continues the one above).
|
|
13
|
+
// Ranges come back the way xterm takes them, 1-based and inclusive. One
|
|
14
|
+
// character is one cell, which holds for anything an address can contain.
|
|
15
|
+
|
|
16
|
+
// The scheme, at least one character after it, and none of the characters that
|
|
17
|
+
// end an address in prose.
|
|
18
|
+
const ADDRESS = /https?:\/\/[^\s"'<>`]+/g;
|
|
19
|
+
// Punctuation that closes the sentence around an address, not the address.
|
|
20
|
+
const TRAILING = /[.,;:!?)\]}'"]+$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {(y: number) => ({ text: string, wrapped: boolean } | undefined)} at
|
|
24
|
+
* @param {number} row 0-based
|
|
25
|
+
* @returns {{ url: string, range: { start: { x: number, y: number }, end: { x: number, y: number } } }[]}
|
|
26
|
+
*/
|
|
27
|
+
export function linksAtRow(at, row) {
|
|
28
|
+
if (!at(row)) return [];
|
|
29
|
+
let top = row;
|
|
30
|
+
while (top > 0 && at(top)?.wrapped && at(top - 1)) top -= 1;
|
|
31
|
+
const rows = [at(top).text];
|
|
32
|
+
for (let y = top + 1; at(y)?.wrapped; y += 1) rows.push(at(y).text);
|
|
33
|
+
|
|
34
|
+
// Where each row starts in the joined string, to turn an index back into a
|
|
35
|
+
// cell.
|
|
36
|
+
const starts = [];
|
|
37
|
+
let joined = '';
|
|
38
|
+
for (const text of rows) { starts.push(joined.length); joined += text; }
|
|
39
|
+
const cell = (i) => {
|
|
40
|
+
let r = starts.length - 1;
|
|
41
|
+
while (r > 0 && starts[r] > i) r -= 1;
|
|
42
|
+
return { x: i - starts[r] + 1, y: top + r + 1 };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const m of joined.matchAll(ADDRESS)) {
|
|
47
|
+
const url = m[0].replace(TRAILING, '');
|
|
48
|
+
if (!/^https?:\/\/./.test(url)) continue;
|
|
49
|
+
const start = cell(m.index);
|
|
50
|
+
const end = cell(m.index + url.length - 1);
|
|
51
|
+
if (start.y > row + 1 || end.y < row + 1) continue;
|
|
52
|
+
out.push({ url, range: { start, end } });
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
@@ -56,3 +56,19 @@ export function commandRunning(processName, exited) {
|
|
|
56
56
|
if (!name) return false;
|
|
57
57
|
return !SHELLS.test(shellName(name));
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
// A COMMAND HAS JUST FINISHED: the terminal went from running something back to
|
|
61
|
+
// its prompt, or the shell ended under it. Settings' Add account panel reloads
|
|
62
|
+
// the accounts on this, because "the account appears above when it is done"
|
|
63
|
+
// was a promise nothing kept: the row read "Not signed in" after `claude auth
|
|
64
|
+
// login` said "Login successful." (2026-10-05). Feed it every reading the
|
|
65
|
+
// terminal polls; it calls `onFinish` once per command, and never for a shell
|
|
66
|
+
// that has only ever sat at its prompt.
|
|
67
|
+
export function whenACommandFinishes(onFinish) {
|
|
68
|
+
let running = false;
|
|
69
|
+
return (processName, exited) => {
|
|
70
|
+
const now = commandRunning(processName, exited);
|
|
71
|
+
if (running && !now) onFinish();
|
|
72
|
+
running = now;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Types for shared/thread-cards.mjs, for the window side.
|
|
2
|
+
import type { ThreadStateWord, WorkItem } from '../renderer/src/types';
|
|
3
|
+
|
|
4
|
+
export const THREAD_STATES: ThreadStateWord[];
|
|
5
|
+
export function threadState(item: Partial<WorkItem> & { claimExpired?: boolean }, now?: number): ThreadStateWord;
|
|
6
|
+
export function firstSentence(text: string | undefined | null, max?: number): string;
|
|
7
|
+
export const SUMMARY_WORDS: number;
|
|
8
|
+
export function wordsIn(text: string | undefined | null): number;
|
|
9
|
+
export const CONTEXT_WORDS: number;
|
|
10
|
+
export const DONE_STEPS: number;
|
|
11
|
+
export const DONE_STEP_WORDS: number;
|
|
12
|
+
export function doneSteps(progress: string | undefined | null): string[];
|
|
13
|
+
export function summaryOf(item: Partial<WorkItem>): { problem: string; progress: string; solution: string; written: boolean };
|
|
14
|
+
export function cardsFor(p: { products: unknown[]; readItems: (product: any) => any[]; now?: number; since?: number | null }): any[];
|
|
15
|
+
type ProjectSeen = { personal?: boolean; seenBy?: string; seenByPeople?: string[] } | null | undefined;
|
|
16
|
+
export function shownToTeam(item: Partial<WorkItem>, since?: number | null, product?: ProjectSeen): boolean;
|
|
17
|
+
export function visibilityOf(item: Partial<WorkItem>, product?: ProjectSeen): 'team' | 'people' | 'private' | undefined;
|
|
18
|
+
export function projectSeenBy(product: ProjectSeen): { who: 'private' | 'team' | 'people'; people: string[] };
|
|
19
|
+
export function shownToPeople(item: Partial<WorkItem>, product?: ProjectSeen): string[];
|