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
package/main/main.mjs
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// The app: the agent inbox. Electron shell around a keyboard-first renderer, a
|
|
2
2
|
// file-derived store, and a supervisor of headless sessions.
|
|
3
3
|
|
|
4
|
-
import { app, BrowserWindow, Menu, Notification, clipboard, crashReporter, dialog, ipcMain, nativeImage, net, powerMonitor, protocol, screen as electronScreen, shell, session } from 'electron';
|
|
4
|
+
import { app, BrowserWindow, Menu, Notification, clipboard, crashReporter, dialog, ipcMain, nativeImage, net, powerMonitor, protocol, screen as electronScreen, shell, session, safeStorage } from 'electron';
|
|
5
|
+
import * as workItemsDisk from './store/work-items.mjs';
|
|
6
|
+
import { createTeamService, teamStateFile } from './team/index.mjs';
|
|
7
|
+
import { loadCloudConfig, supabaseSession } from './team/session.mjs';
|
|
8
|
+
// MY WORKSPACE IS NOT A TEAM FEATURE ANY MORE (w-f8d123be62), even though it
|
|
9
|
+
// still lives in the team folder next to the sharing rules it has to obey.
|
|
10
|
+
import { ensurePersonalProject } from './team/projects.mjs';
|
|
5
11
|
import path from 'node:path';
|
|
6
12
|
import fs from 'node:fs';
|
|
7
13
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -15,19 +21,22 @@ import { storeRootEnv } from './store/home.mjs';
|
|
|
15
21
|
import { Supervisor } from './supervisor.mjs';
|
|
16
22
|
import { startCodexWatch } from './codex-watch.mjs';
|
|
17
23
|
import { registerIpc } from './ipc.mjs';
|
|
18
|
-
import { carryHerBriefsAcross, joinMessageRules } from './instruction-settings.mjs';
|
|
24
|
+
import { carryHerBriefsAcross, joinMessageRules, setAsideShippedMessageRules } from './instruction-settings.mjs';
|
|
19
25
|
import * as approvals from './approvals.mjs';
|
|
20
26
|
import { recoveryToast } from '../shared/recovery.mjs';
|
|
21
27
|
import { dataFolderName, isNewUserBuild } from '../shared/side-build.mjs';
|
|
22
28
|
import { runningAsAFreshUser } from '../shared/fresh-user-home.mjs';
|
|
23
29
|
import { openFreshUser } from './fresh-user.mjs';
|
|
24
30
|
import { installCrashReports, reportFromRenderer, pending as pendingCrashes, setTransport } from './crash-report.mjs';
|
|
25
|
-
import { createAnalytics } from './analytics.mjs';
|
|
31
|
+
import { createAnalytics, createDailyCount } from './analytics.mjs';
|
|
26
32
|
import { createUpdater } from './updater.mjs';
|
|
33
|
+
import { createSourceUpdater } from './source-updater.mjs';
|
|
27
34
|
import { installNotifier } from './notify.mjs';
|
|
28
35
|
import { DOC_SCHEMES, DocGrants, docPath } from './doc-scheme.mjs';
|
|
29
|
-
import {
|
|
36
|
+
import { quietTheFramesScrollbars } from './frame-scrollbars.mjs';
|
|
37
|
+
import { IMG_SCHEMES, imgPath, mediaResponse, mediaType, servable } from './img-scheme.mjs';
|
|
30
38
|
import { hotWindowVerdict, storeHasWork } from './dev-window.mjs';
|
|
39
|
+
import { writeHeldThenReload } from './write-before-reload.mjs';
|
|
31
40
|
import { IS_SHE_TYPING_IN_THE_PAGE, theAppKeepsThisInput, theAppKeepsThisLetterInAPage } from '../shared/artifact-keys.mjs';
|
|
32
41
|
import { NAME, Name, WAS } from '../shared/product-name.mjs';
|
|
33
42
|
|
|
@@ -99,6 +108,15 @@ const appDir = path.join(__dirname, '..');
|
|
|
99
108
|
// live. This has to happen before ANYTHING reads a path, because everything
|
|
100
109
|
// downstream keys off app.getName, the single-instance lock at the foot of
|
|
101
110
|
// this file included.
|
|
111
|
+
// THE TEAM VERSION RUNS BESIDE THE ONE IN USE TODAY (2026-10-01). Run from
|
|
112
|
+
// source, this checkout is named exactly like the solo app, so it would read
|
|
113
|
+
// her data folder, take her single-instance lock and quit on the spot. A
|
|
114
|
+
// profile (`AGENTBOX_PROFILE=team npx electron .`) gives it a name of its own,
|
|
115
|
+
// and with the name a data folder and a lock of its own. Letters, digits and
|
|
116
|
+
// dashes only, and never a space: "agentbox team" reads as a side build, and
|
|
117
|
+
// side builds share the real data folder on purpose.
|
|
118
|
+
const PROFILE = String(process.env.AGENTBOX_PROFILE ?? '').trim().replace(/[^a-z0-9-]/gi, '');
|
|
119
|
+
if (PROFILE) app.setName(`${NAME}-${PROFILE}`);
|
|
102
120
|
if (dataFolderName(app.getName()) !== app.getName()) app.setName(dataFolderName(app.getName()));
|
|
103
121
|
|
|
104
122
|
// "ASTRAL NEW USER" IS A STRANGER EVERY TIME IT OPENS.
|
|
@@ -132,7 +150,8 @@ if (isNewUserBuild(app.getName()) && !runningAsAFreshUser()) {
|
|
|
132
150
|
// existing user's folder is called the app, not the app, so a bare the app check would
|
|
133
151
|
// have found nothing and handed them an empty Powerup. Whoever renames this app
|
|
134
152
|
// a third time adds one entry here and nothing else.
|
|
135
|
-
|
|
153
|
+
// A profile never falls back to an older folder: an older folder is hers.
|
|
154
|
+
if (!PROFILE) try {
|
|
136
155
|
const named = path.join(app.getPath('appData'), app.getName());
|
|
137
156
|
if (!fs.existsSync(named)) {
|
|
138
157
|
// THESE ARE HISTORY AND NOT COPY. They are the folder names this app has
|
|
@@ -184,6 +203,9 @@ try {
|
|
|
184
203
|
// supervisor exists, so nothing has read the new name yet.
|
|
185
204
|
const joined = joinMessageRules(appDir, userDir);
|
|
186
205
|
if (joined) console.log(`zero: joined the message rules into one file, ${joined.total} characters`);
|
|
206
|
+
// AND A BOX THAT AN OLDER JOIN FILLED WITH ONLY OUR TEXT IS EMPTIED, ONCE
|
|
207
|
+
// (w-3ec9f07978). Kept as a restore point; our rules ride from the checkout.
|
|
208
|
+
if (setAsideShippedMessageRules(appDir, userDir)) console.log('zero: the message rules box held only the shipped rules; set aside as a restore point');
|
|
187
209
|
} catch (err) {
|
|
188
210
|
console.warn(`zero: could not carry her instructions into ${userDir}: ${err.message}`);
|
|
189
211
|
}
|
|
@@ -235,8 +257,18 @@ function reloadRenderer() {
|
|
|
235
257
|
// identical build unless something records that it arrived at all.
|
|
236
258
|
console.log(`zero: reload requested, renderer built ${new Date(builtAt()).toLocaleTimeString()}`);
|
|
237
259
|
if (!window || window.isDestroyed()) return;
|
|
238
|
-
|
|
239
|
-
|
|
260
|
+
// The page may be holding something she did in the last three seconds (a
|
|
261
|
+
// close, an answer, a reply) for Z to take back. It is written first, or the
|
|
262
|
+
// reload throws it away and the thread comes back to her inbox
|
|
263
|
+
// (w-47218417a5). See write-before-reload.mjs.
|
|
264
|
+
void writeHeldThenReload({
|
|
265
|
+
webContents: window.webContents,
|
|
266
|
+
ipcMain,
|
|
267
|
+
reload: () => {
|
|
268
|
+
reloadRequested = true;
|
|
269
|
+
window.webContents.reloadIgnoringCache();
|
|
270
|
+
},
|
|
271
|
+
});
|
|
240
272
|
}
|
|
241
273
|
|
|
242
274
|
async function createWindow() {
|
|
@@ -290,6 +322,8 @@ async function createWindow() {
|
|
|
290
322
|
// died are finally sent, because a report is written while the process is
|
|
291
323
|
// dying and delivery cannot be synchronous.
|
|
292
324
|
const analytics = createAnalytics({ config, version: app.getVersion(), dir: crashDir });
|
|
325
|
+
// "Used today", once a day, at launch and whenever the window comes forward.
|
|
326
|
+
const usedToday = createDailyCount((name) => analytics.track(name));
|
|
293
327
|
// The seam is filled whenever there is somewhere to send to. Whether anything
|
|
294
328
|
// actually goes is asked again inside, at every send, so her switch stops it
|
|
295
329
|
// from the moment she moves it rather than at the next launch.
|
|
@@ -298,10 +332,23 @@ async function createWindow() {
|
|
|
298
332
|
const drained = analytics.drainCrashes(pendingCrashes());
|
|
299
333
|
if (drained) console.log(`zero: sent ${drained} crash report(s) from a previous launch`);
|
|
300
334
|
analytics.track('app_opened');
|
|
335
|
+
usedToday(Date.now());
|
|
301
336
|
} else {
|
|
302
337
|
console.log(`zero: diagnostics is ${analytics.reason}, nothing is sent`);
|
|
303
338
|
}
|
|
304
339
|
|
|
340
|
+
// EVERYBODY HAS SOMEWHERE TO SEND A THREAD (w-f8d123be62, approved
|
|
341
|
+
// 2026-10-05). My Workspace used to be made inside `signedIn()`, so the
|
|
342
|
+
// single-person app — a Mac with no team cloud, which never builds the team
|
|
343
|
+
// service at all — opened on no projects whatsoever: the chip read "No
|
|
344
|
+
// project", Send was off, and nothing said why. It is made HERE, before the
|
|
345
|
+
// store reads the account root, so the first list of projects already has it
|
|
346
|
+
// and no second pass is needed. Made once and never remade; a Mac that cannot
|
|
347
|
+
// write it still starts, and the card now says so out loud rather than
|
|
348
|
+
// leaving a dead button (renderer/src/compose-says.ts noProjectYet).
|
|
349
|
+
try {
|
|
350
|
+
ensurePersonalProject(config.accountRoot);
|
|
351
|
+
} catch (err) { console.warn(`zero: could not make My Workspace: ${err.message}`); }
|
|
305
352
|
const store = await new Store(config).init();
|
|
306
353
|
// Where the window's own pictures may come from (main/img-scheme.mjs). The
|
|
307
354
|
// account root holds every product's docs, attachments, designs and the
|
|
@@ -310,7 +357,42 @@ async function createWindow() {
|
|
|
310
357
|
// request so a project made after launch is not a folder of broken images.
|
|
311
358
|
imageRoots = () => [config.accountRoot, ...store.listProducts().map((p) => p.repoPath).filter(Boolean)];
|
|
312
359
|
const supervisor = new Supervisor(config, store, appDir, dataDir, userDir);
|
|
360
|
+
// THAT agents start and stop work (main/analytics.mjs checks the switch).
|
|
361
|
+
supervisor.count = (name, props) => analytics.track(name, props);
|
|
313
362
|
app.on('before-quit', () => supervisor.killAll());
|
|
363
|
+
// THE TEAM. Off entirely on a build with no team cloud (the single-person
|
|
364
|
+
// app). Otherwise it restores whoever was signed in, and from then on every
|
|
365
|
+
// line this Mac writes says who wrote it and shared projects stay in sync.
|
|
366
|
+
// Its session file sits beside the store, encrypted with the Mac's own
|
|
367
|
+
// keychain-backed key, so each store root is its own signed-in person.
|
|
368
|
+
// THE KEYCHAIN IS ONLY ASKED WHEN THERE IS A TEAM (2026-10-06): asking it at
|
|
369
|
+
// every launch put a "Agentbox Safe Storage" password prompt in front of the
|
|
370
|
+
// single-person app, which has no sign-in to keep
|
|
371
|
+
// (tests/the-installed-app-is-the-single-person-app.test.mjs).
|
|
372
|
+
const cloudConfig = loadCloudConfig(appDir, { packaged: app.isPackaged });
|
|
373
|
+
const team = cloudConfig ? createTeamService({
|
|
374
|
+
session: supabaseSession({
|
|
375
|
+
cloudConfig,
|
|
376
|
+
sessionFile: path.join(config.storeRoot, '.team-session'),
|
|
377
|
+
...(safeStorage.isEncryptionAvailable()
|
|
378
|
+
? { encrypt: (text) => safeStorage.encryptString(text), decrypt: (buf) => safeStorage.decryptString(buf) }
|
|
379
|
+
: { encrypt: null, decrypt: null }),
|
|
380
|
+
packaged: app.isPackaged,
|
|
381
|
+
openExternal: (url) => shell.openExternal(url),
|
|
382
|
+
}),
|
|
383
|
+
store,
|
|
384
|
+
disk: workItemsDisk,
|
|
385
|
+
accountRoot: config.accountRoot,
|
|
386
|
+
stateFile: teamStateFile(config.storeRoot),
|
|
387
|
+
// The window's push is declared further down and wired once the window
|
|
388
|
+
// exists; until then a change has nobody to tell.
|
|
389
|
+
onChange: () => { try { pushUpdate(); } catch { /* no window yet */ } },
|
|
390
|
+
log: (line) => console.log(line),
|
|
391
|
+
}) : null;
|
|
392
|
+
if (team) {
|
|
393
|
+
team.start().then(() => supervisor.wake?.()).catch((err) => console.warn(`team: ${err.message}`));
|
|
394
|
+
app.on('before-quit', () => team.stop());
|
|
395
|
+
}
|
|
314
396
|
// HER CODEX CONVERSATIONS ASK TO COME IN. Once a minute: a new one in a
|
|
315
397
|
// folder a project points at gets a row asking yes or no, and the ones she
|
|
316
398
|
// said yes to follow Codex's latest answer.
|
|
@@ -542,6 +624,7 @@ async function createWindow() {
|
|
|
542
624
|
lastScreenDetail = next;
|
|
543
625
|
window.webContents.send('zero:screen-detail', { detail: next });
|
|
544
626
|
};
|
|
627
|
+
window.on('focus', () => usedToday(Date.now()));
|
|
545
628
|
window.on('move', tellScreenDetail);
|
|
546
629
|
window.on('moved', tellScreenDetail);
|
|
547
630
|
electronScreen.on('display-metrics-changed', tellScreenDetail);
|
|
@@ -566,9 +649,17 @@ async function createWindow() {
|
|
|
566
649
|
// `pushUpdate` is filled in on the next line, because the updater has to
|
|
567
650
|
// exist before the IPC surface that reports it and the IPC surface is what
|
|
568
651
|
// knows how to reach the window.
|
|
652
|
+
//
|
|
653
|
+
// AND A COPY RUN FROM SOURCE watches its git branch instead of a release
|
|
654
|
+
// feed (main/source-updater.mjs), because that is how teammates run the team
|
|
655
|
+
// build and their updates are pushes to main, not releases.
|
|
569
656
|
let pushUpdate = () => {};
|
|
570
|
-
const updater = createUpdater({
|
|
657
|
+
const updater = app.isPackaged ? createUpdater({
|
|
658
|
+
app,
|
|
659
|
+
onChanged: () => pushUpdate(),
|
|
660
|
+
}) : createSourceUpdater({
|
|
571
661
|
app,
|
|
662
|
+
appDir,
|
|
572
663
|
onChanged: () => pushUpdate(),
|
|
573
664
|
});
|
|
574
665
|
|
|
@@ -577,6 +668,7 @@ async function createWindow() {
|
|
|
577
668
|
const ipc = registerIpc({
|
|
578
669
|
store, supervisor, config, window, analytics, docGrants, updater,
|
|
579
670
|
host: { ipcMain, app, dialog },
|
|
671
|
+
team,
|
|
580
672
|
});
|
|
581
673
|
pushUpdate = ipc.push;
|
|
582
674
|
// The Agents menu's Allow/Deny, bound to the real door now that there is one.
|
|
@@ -584,6 +676,9 @@ async function createWindow() {
|
|
|
584
676
|
// Codex card.
|
|
585
677
|
answerApproval = ipc.answerApproval;
|
|
586
678
|
updater.start();
|
|
679
|
+
// A task the app just shipped moved main: look now rather than in half an
|
|
680
|
+
// hour, so the restart is offered while she still remembers the task.
|
|
681
|
+
supervisor.onShipped = () => { try { updater.check?.(); } catch {} };
|
|
587
682
|
app.on('before-quit', () => updater.stop());
|
|
588
683
|
|
|
589
684
|
// Cmd+Y / Cmd+N answer the oldest approval card from ANYWHERE, including
|
|
@@ -713,9 +808,7 @@ async function createWindow() {
|
|
|
713
808
|
//
|
|
714
809
|
// THIS LINE IS LOAD-BEARING: two tests slice main.mjs from the input handler
|
|
715
810
|
// above to these words to read the chord wiring on its own, so the words at
|
|
716
|
-
// the start of it are not free to change.
|
|
717
|
-
// because what stood here was the emulator, its save vault and the shelf that
|
|
718
|
-
// reached them. All of that is deleted; see decisions.md.
|
|
811
|
+
// the start of it are not free to change.
|
|
719
812
|
|
|
720
813
|
store.watch();
|
|
721
814
|
// ZERO_NO_SUPERVISOR: run the inbox without spawning any sessions (UI work,
|
|
@@ -802,6 +895,10 @@ async function createWindow() {
|
|
|
802
895
|
webPreferences.autoplayPolicy = 'document-user-activation-required';
|
|
803
896
|
});
|
|
804
897
|
|
|
898
|
+
// A page in the pane hides its scrollbar until you scroll, like the app's own
|
|
899
|
+
// (frame-scrollbars.mjs). Without it, a mouse on macOS gets a white track.
|
|
900
|
+
quietTheFramesScrollbars(window.webContents);
|
|
901
|
+
|
|
805
902
|
// Escape must ALWAYS leave the junk browser, even when the guest page has
|
|
806
903
|
// focus and would otherwise swallow the key. Intercept it below the page.
|
|
807
904
|
app.on('web-contents-created', (_event, contents) => {
|
|
@@ -917,6 +1014,9 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
917
1014
|
}
|
|
918
1015
|
});
|
|
919
1016
|
app.whenReady().then(() => {
|
|
1017
|
+
// THE ABOUT BOX SAYS AGENTBOX AND ITS OWN VERSION (w-db6f5e331e). Run from
|
|
1018
|
+
// source it read the bundle's, which is Electron's: "Electron 43.0.0".
|
|
1019
|
+
app.setAboutPanelOptions({ applicationName: NAME, applicationVersion: app.getVersion(), version: '' });
|
|
920
1020
|
// Serve a granted file, and nothing else. A page under this scheme is one
|
|
921
1021
|
// of ours, but it is written by an agent and read by her, so the check is
|
|
922
1022
|
// on every request rather than only on the one the pane asked for.
|
|
@@ -930,9 +1030,9 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
930
1030
|
// Nothing is injected into a page any more; see doc-scheme.mjs for the
|
|
931
1031
|
// pinch zoom that briefly lived here and why she turned it down.
|
|
932
1032
|
//
|
|
933
|
-
// NO-STORE, OR A REDRAWN PICTURE NEVER REACHES
|
|
934
|
-
//
|
|
935
|
-
//
|
|
1033
|
+
// NO-STORE, OR A REDRAWN PICTURE NEVER REACHES THE READER (w-3fc39983be).
|
|
1034
|
+
// The font in redrawn pictures looked unchanged, and that was a bug.
|
|
1035
|
+
// An agent redrew six pictures under the same names;
|
|
936
1036
|
// the page's html came back new and every <img> in it came back from
|
|
937
1037
|
// the window's memory, round one's pictures under round two's captions,
|
|
938
1038
|
// until a restart. Measured in a hidden probe of this exact setup
|
|
@@ -952,7 +1052,11 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
952
1052
|
const file = imgPath(request.url);
|
|
953
1053
|
if (!file || !servable(file, imageRoots())) return new Response('Not available.', { status: 403 });
|
|
954
1054
|
try {
|
|
955
|
-
|
|
1055
|
+
// A sound or a film is answered in the piece the player asked for, or
|
|
1056
|
+
// it plays but cannot be moved (mediaResponse, main/img-scheme.mjs).
|
|
1057
|
+
const res = mediaType(file)
|
|
1058
|
+
? await mediaResponse(file, request.headers.get('range'))
|
|
1059
|
+
: await net.fetch(pathToFileURL(file).toString());
|
|
956
1060
|
// The window is allowed to read its own pictures back out, which is
|
|
957
1061
|
// what a copy needs. This widens nothing: the two rules above already
|
|
958
1062
|
// decided this file may be seen at all, and only this window can ask on
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// WHICH COMMANDS ARE HEAVY ON THIS MAC, LEARNED FROM WHAT THEY DID (w-3958c3753d).
|
|
2
|
+
//
|
|
3
|
+
// NOTHING HERE KNOWS WHAT ANY PROGRAM IS. The first proposal for holding heavy
|
|
4
|
+
// work back matched names (npm, pytest, xcodebuild) and the founder turned it
|
|
5
|
+
// down: another package manager, a script run directly, a training job, and a
|
|
6
|
+
// list of names is wrong. So a command is filed under what it is (its program
|
|
7
|
+
// and the first word after it that is not a flag) and judged by what it was
|
|
8
|
+
// measured using, here, the last few times it ran.
|
|
9
|
+
//
|
|
10
|
+
// THE ONE RULE THAT IS NOT NEGOTIABLE: missing evidence never makes a command
|
|
11
|
+
// light. Light means "seen light, more than once, and never heavy since". One
|
|
12
|
+
// heavy run is enough to make that exact command heavy. A program that has been
|
|
13
|
+
// light every time across many different arguments lends that to an argument
|
|
14
|
+
// it has not seen, and stops the moment one of its runs is not light.
|
|
15
|
+
//
|
|
16
|
+
// A RUN THAT ENDED INSIDE 2 SECONDS AND WAS NEVER SEEN BIG COUNTS AS LIGHT.
|
|
17
|
+
// Measured on 2026-10-04 across 30 recent agent sessions (2006 commands): the
|
|
18
|
+
// median command ran 1.3 s, so most finish between two samples and would never
|
|
19
|
+
// be measured at all. A command that short cannot hold memory long enough to
|
|
20
|
+
// starve anything, which is the whole of what this gate is for. A command sent
|
|
21
|
+
// to the background is the exception: its tool call returns at once while its
|
|
22
|
+
// process carries on, so its quickness proves nothing.
|
|
23
|
+
//
|
|
24
|
+
// The five lines of shell knowledge below are about the SHELL, not about any
|
|
25
|
+
// program: builtins start nothing, and time/nice/env/timeout/nohup only run the
|
|
26
|
+
// command after them.
|
|
27
|
+
|
|
28
|
+
const BUILTINS = new Set(['cd', 'export', 'echo', 'printf', 'true', 'false', 'test', '[', '[[', ']]', 'set', 'unset',
|
|
29
|
+
'pwd', 'read', 'exit', 'return', 'shift', 'wait', ':', 'alias', 'type', 'local', 'declare', 'pushd', 'popd', 'umask']);
|
|
30
|
+
const WRAPPERS = new Set(['time', 'nice', 'env', 'timeout', 'nohup', 'caffeinate', 'exec', 'command']);
|
|
31
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
32
|
+
const REDIRECT = /^\d*(>>?|<<?<?|&>>?|>&|<&)/;
|
|
33
|
+
|
|
34
|
+
/** Light below this many MB at its sampled peak, heavy at or above HEAVY_MB. */
|
|
35
|
+
export const LIGHT_MB = 150;
|
|
36
|
+
export const HEAVY_MB = 400;
|
|
37
|
+
/** A run this short, never seen big, counts as light. */
|
|
38
|
+
export const QUICK_MS = 2_000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Split a shell command into the programs it starts, as `program firstArg`
|
|
42
|
+
* keys, without running or fully parsing it. `opaque` is true when part of it
|
|
43
|
+
* is computed at run time ($(...) or backticks outside single quotes), which
|
|
44
|
+
* this cannot see into.
|
|
45
|
+
*/
|
|
46
|
+
export function commandSegments(command) {
|
|
47
|
+
const text = String(command ?? '');
|
|
48
|
+
const segments = [];
|
|
49
|
+
let words = [];
|
|
50
|
+
let word = '';
|
|
51
|
+
let inWord = false;
|
|
52
|
+
let quote = null;
|
|
53
|
+
let opaque = false;
|
|
54
|
+
const heredocs = [];
|
|
55
|
+
const endWord = () => {
|
|
56
|
+
if (inWord) words.push(word);
|
|
57
|
+
word = ''; inWord = false;
|
|
58
|
+
};
|
|
59
|
+
const endSegment = () => { endWord(); if (words.length) segments.push(words); words = []; };
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < text.length; i++) {
|
|
62
|
+
const c = text[i];
|
|
63
|
+
if (quote === "'") {
|
|
64
|
+
if (c === "'") quote = null; else word += c;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (c === '\\' && i + 1 < text.length) { word += text[++i]; inWord = true; continue; }
|
|
68
|
+
if (quote === '"') {
|
|
69
|
+
if (c === '"') quote = null;
|
|
70
|
+
else {
|
|
71
|
+
if (c === '`' || (c === '$' && text[i + 1] === '(')) opaque = true;
|
|
72
|
+
word += c;
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (c === "'" || c === '"') { quote = c; inWord = true; continue; }
|
|
77
|
+
if (c === '`' || (c === '$' && text[i + 1] === '(')) opaque = true;
|
|
78
|
+
if (c === '<' && text[i + 1] === '<' && text[i + 2] !== '<') {
|
|
79
|
+
// A here-document: its body is text handed to a program, not commands.
|
|
80
|
+
endWord();
|
|
81
|
+
let j = i + 2;
|
|
82
|
+
if (text[j] === '-') j++;
|
|
83
|
+
while (text[j] === ' ') j++;
|
|
84
|
+
let delim = '';
|
|
85
|
+
while (j < text.length && !/[\s;&|]/.test(text[j])) delim += text[j++];
|
|
86
|
+
heredocs.push(delim.replace(/^['"]|['"]$/g, ''));
|
|
87
|
+
i = j - 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (c === '\n') {
|
|
91
|
+
endSegment();
|
|
92
|
+
while (heredocs.length) {
|
|
93
|
+
const delim = heredocs.shift();
|
|
94
|
+
let end = text.indexOf('\n', i + 1);
|
|
95
|
+
while (true) {
|
|
96
|
+
const line = text.slice(i + 1, end === -1 ? text.length : end);
|
|
97
|
+
i = end === -1 ? text.length : end;
|
|
98
|
+
if (line.trim() === delim || end === -1) break;
|
|
99
|
+
end = text.indexOf('\n', i + 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (c === ';' || c === '|' || (c === '&' && !/[<>]$/.test(word) && text[i + 1] !== '>')) {
|
|
105
|
+
endSegment();
|
|
106
|
+
if ((c === '|' || c === '&') && text[i + 1] === c) i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (c === ' ' || c === '\t') { endWord(); continue; }
|
|
110
|
+
word += c; inWord = true;
|
|
111
|
+
}
|
|
112
|
+
endSegment();
|
|
113
|
+
|
|
114
|
+
const keys = [];
|
|
115
|
+
for (const seg of segments) {
|
|
116
|
+
const key = keyOf(seg);
|
|
117
|
+
if (key) keys.push(key);
|
|
118
|
+
}
|
|
119
|
+
return { keys, opaque };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function keyOf(segmentWords) {
|
|
123
|
+
const words = [];
|
|
124
|
+
for (let i = 0; i < segmentWords.length; i++) {
|
|
125
|
+
const w = segmentWords[i];
|
|
126
|
+
if (REDIRECT.test(w)) {
|
|
127
|
+
// `> out.txt` carries its target as the next word; `>out.txt` does not.
|
|
128
|
+
if (/^\d*(>>?|<|&>>?)$/.test(w)) i++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (w === '(' || w === ')' || w === '{' || w === '}' || w === '!') continue;
|
|
132
|
+
words.push(w);
|
|
133
|
+
}
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < words.length && ASSIGNMENT.test(words[i])) i++;
|
|
136
|
+
while (i < words.length && WRAPPERS.has(basename(words[i]))) {
|
|
137
|
+
i++;
|
|
138
|
+
while (i < words.length && (words[i].startsWith('-') || /^\d+(\.\d+)?[smhd]?$/.test(words[i]) || ASSIGNMENT.test(words[i]))) i++;
|
|
139
|
+
}
|
|
140
|
+
if (i >= words.length) return null;
|
|
141
|
+
const program = basename(words[i]);
|
|
142
|
+
if (!program || BUILTINS.has(program)) return null;
|
|
143
|
+
const arg = words.slice(i + 1).find((w) => !w.startsWith('-'));
|
|
144
|
+
return arg ? `${program} ${arg.slice(0, 80)}` : program;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function basename(p) {
|
|
148
|
+
const s = String(p);
|
|
149
|
+
const at = s.lastIndexOf('/');
|
|
150
|
+
return at === -1 ? s : s.slice(at + 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const programOf = (key) => key.split(' ')[0];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The learned record. Each key and each program keeps its last ten
|
|
157
|
+
* observations as letters: l light, m in between, h heavy, ? not measured.
|
|
158
|
+
* Both maps are kept in least-recently-seen-first order so the oldest falls off
|
|
159
|
+
* first when either passes `maxKeys`.
|
|
160
|
+
*/
|
|
161
|
+
export class CommandHistory {
|
|
162
|
+
constructor({ lightMb = LIGHT_MB, heavyMb = HEAVY_MB, maxKeys = 2_000 } = {}) {
|
|
163
|
+
this.lightMb = lightMb;
|
|
164
|
+
this.heavyMb = heavyMb;
|
|
165
|
+
this.maxKeys = maxKeys;
|
|
166
|
+
this.keys = new Map();
|
|
167
|
+
this.programs = new Map();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** What one finished run tells us. `peakMb` null means it was never sampled. */
|
|
171
|
+
record(command, { peakMb = null, durationMs = null, background = false } = {}) {
|
|
172
|
+
const { keys, opaque } = commandSegments(command);
|
|
173
|
+
if (!keys.length) return;
|
|
174
|
+
let obs;
|
|
175
|
+
if (typeof peakMb === 'number' && Number.isFinite(peakMb)) {
|
|
176
|
+
obs = peakMb >= this.heavyMb ? 'h' : peakMb < this.lightMb ? 'l' : 'm';
|
|
177
|
+
} else {
|
|
178
|
+
obs = !background && typeof durationMs === 'number' && durationMs < QUICK_MS ? 'l' : '?';
|
|
179
|
+
}
|
|
180
|
+
// A chain's memory belongs to the chain: `git status && npm test` measured
|
|
181
|
+
// heavy says nothing bad about `git status`. Only a light chain vouches for
|
|
182
|
+
// each of its parts.
|
|
183
|
+
if (keys.length > 1 || opaque) {
|
|
184
|
+
this._push(this.keys, chainKey(keys, opaque), obs);
|
|
185
|
+
if (obs !== 'l') return;
|
|
186
|
+
}
|
|
187
|
+
for (const key of keys) {
|
|
188
|
+
this._push(this.keys, key, obs);
|
|
189
|
+
this._push(this.programs, programOf(key), obs);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 'light' | 'unknown' | 'heavy' for a command about to run. */
|
|
194
|
+
classify(command) {
|
|
195
|
+
const { keys, opaque } = commandSegments(command);
|
|
196
|
+
if (!keys.length && !opaque) return 'light';
|
|
197
|
+
let verdict = 'light';
|
|
198
|
+
const worse = (v) => {
|
|
199
|
+
if (v === 'heavy' || verdict === 'heavy') verdict = 'heavy';
|
|
200
|
+
else if (v === 'unknown') verdict = 'unknown';
|
|
201
|
+
};
|
|
202
|
+
if (keys.length > 1 || opaque) {
|
|
203
|
+
const chain = this.keys.get(chainKey(keys, opaque));
|
|
204
|
+
if (chain?.slice(-5).includes('h')) return 'heavy';
|
|
205
|
+
if (opaque) worse('unknown');
|
|
206
|
+
}
|
|
207
|
+
for (const key of keys) worse(this._classifyKey(key));
|
|
208
|
+
return verdict;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_classifyKey(key) {
|
|
212
|
+
const own = this.keys.get(key);
|
|
213
|
+
const recent = own?.slice(-5) ?? [];
|
|
214
|
+
if (recent.includes('h')) return 'heavy';
|
|
215
|
+
if (own && own.length >= 2 && recent.every((o) => o === 'l')) return 'light';
|
|
216
|
+
if (recent.some((o) => o !== 'l')) return 'unknown';
|
|
217
|
+
const prog = this.programs.get(programOf(key));
|
|
218
|
+
if (prog && prog.length >= 5 && prog.slice(-10).every((o) => o === 'l')) return 'light';
|
|
219
|
+
return 'unknown';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_push(map, key, obs) {
|
|
223
|
+
const list = map.get(key) ?? [];
|
|
224
|
+
map.delete(key);
|
|
225
|
+
list.push(obs);
|
|
226
|
+
map.set(key, list.slice(-10));
|
|
227
|
+
while (map.size > this.maxKeys) map.delete(map.keys().next().value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
toJSON() {
|
|
231
|
+
return { v: 1, keys: [...this.keys], programs: [...this.programs] };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Anything unreadable is an empty history, which only means "unknown". */
|
|
235
|
+
static fromJSON(raw, opts) {
|
|
236
|
+
const h = new CommandHistory(opts);
|
|
237
|
+
if (!raw || raw.v !== 1) return h;
|
|
238
|
+
const load = (map, entries) => {
|
|
239
|
+
if (!Array.isArray(entries)) return;
|
|
240
|
+
for (const e of entries) {
|
|
241
|
+
if (Array.isArray(e) && typeof e[0] === 'string' && Array.isArray(e[1])) {
|
|
242
|
+
map.set(e[0], e[1].filter((o) => ['l', 'm', 'h', '?'].includes(o)).slice(-10));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
load(h.keys, raw.keys);
|
|
247
|
+
load(h.programs, raw.programs);
|
|
248
|
+
return h;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function chainKey(keys, opaque) {
|
|
253
|
+
return `${opaque ? '$( ' : ''}${keys.join(' && ')}`;
|
|
254
|
+
}
|