workoffdesk 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +5 -3
- package/dist/build.json +1 -1
- package/dist/server/agents/claude/screen/background.js +7 -0
- package/dist/server/agents/claude/screen/prompt.js +34 -0
- package/dist/server/agents/claude/screen/screen.js +42 -3
- package/dist/server/agents/claude/terminals.js +67 -12
- package/dist/server/agents/claude/transcript/chat.js +98 -135
- package/dist/server/agents/claude/transcript/events.js +158 -0
- package/dist/server/agents/claude/transcript/transcriptFile.js +50 -10
- package/dist/server/agents/claude/transcript/transcripts.js +118 -30
- package/dist/server/agents/claude/transcript/turn.js +40 -61
- package/dist/server/agents/codex/agent.js +9 -2
- package/dist/server/agents/codex/events.js +107 -0
- package/dist/server/agents/codex/items.js +54 -74
- package/dist/server/agents/codex/notifications.js +14 -13
- package/dist/server/agents/codex/threads.js +54 -40
- package/dist/server/agents/events.js +4 -0
- package/dist/server/agents/turnSays.js +58 -0
- package/dist/server/cli.js +2 -0
- package/dist/server/commands/activity.js +28 -0
- package/dist/server/http/app.js +8 -4
- package/dist/server/http/auth/guard.js +8 -4
- package/dist/server/http/auth/passkey.js +6 -3
- package/dist/server/http/routes/appUpdate.js +1 -1
- package/dist/server/http/routes/chat.js +10 -0
- package/dist/server/http/routes/claude.js +70 -16
- package/dist/server/http/routes/codex.js +38 -3
- package/dist/server/http/routes/journal.js +3 -1
- package/dist/server/http/routes/push.js +36 -5
- package/dist/server/http/routes/pushRelay.js +62 -0
- package/dist/server/http/routes/sessions.js +32 -1
- package/dist/server/http/serve.js +8 -2
- package/dist/server/journal/db.js +4 -0
- package/dist/server/journal/index.js +4 -0
- package/dist/server/journal/ingest.js +26 -13
- package/dist/server/notify/apns.js +12 -7
- package/dist/server/notify/push.js +30 -4
- package/dist/server/notify/seal.js +12 -0
- package/dist/server/ops/doctor.js +7 -6
- package/dist/server/ops/problems.js +12 -7
- package/dist/server/sessions/control.js +65 -0
- package/dist/server/sessions/events.js +7 -5
- package/dist/server/sessions/machine/input.js +21 -2
- package/dist/server/sessions/machine/lifecycle.js +151 -81
- package/dist/server/sessions/machine/moves.js +65 -13
- package/dist/server/sessions/machine/naming.js +85 -0
- package/dist/server/sessions/machine/state.js +2 -0
- package/dist/server/sessions/machine/status.js +1 -2
- package/dist/server/sessions/machine/takeOvers.js +92 -0
- package/dist/server/sessions/machine.js +87 -23
- package/dist/server/sessions/outbox.js +7 -1
- package/dist/server/system/config.js +16 -1
- package/dist/server/system/errors.js +2 -2
- package/dist/server/system/messages.js +14 -176
- package/dist/server/system/messages.zh.js +186 -0
- package/dist/server/system/project.js +65 -0
- package/dist/server/system/store.js +1 -1
- package/dist/server/system/tmux.js +1 -1
- package/dist/web/assets/{Setup-C27bNf5I.js → Setup-OaIjxfwl.js} +1 -1
- package/dist/web/assets/main-DFndf3Z8.js +132 -0
- package/dist/web/assets/main-mChS_ic8.css +1 -0
- package/dist/web/icon-180.png +0 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/icon-maskable-512.png +0 -0
- package/dist/web/icon.svg +14 -6
- package/dist/web/index.html +2 -2
- package/dist/web/manifest.webmanifest +1 -1
- package/dist/web/sw.js +4 -4
- package/package.json +3 -3
- package/dist/web/assets/main-C8GiLoJy.css +0 -1
- package/dist/web/assets/main-DQFM4yJU.js +0 -132
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { InvalidRequest } from '../../system/errors.js';
|
|
2
|
+
// How often a conversation waited on is looked at on the Mac, and for how long at most.
|
|
3
|
+
const TAKE_OVER_LOOK_MS = 2_000;
|
|
4
|
+
const TAKE_OVER_WAIT_MS = 12 * 3600_000;
|
|
5
|
+
// The step a take-over waits for is done once the conversation is open nowhere, or its turn is over there.
|
|
6
|
+
const stepDone = (s) => !s || s === 'idle' || s === 'background';
|
|
7
|
+
// Conversations open in a terminal on the Mac, to be taken over once the step there is done. Each is kept in the
|
|
8
|
+
// store while it is waited on, so a server started again goes on waiting for it.
|
|
9
|
+
export class TakeOvers {
|
|
10
|
+
s;
|
|
11
|
+
d;
|
|
12
|
+
p;
|
|
13
|
+
// Per conversation waited on, the way to stop waiting.
|
|
14
|
+
waits = new Map();
|
|
15
|
+
constructor(s, d, p) {
|
|
16
|
+
this.s = s;
|
|
17
|
+
this.d = d;
|
|
18
|
+
this.p = p;
|
|
19
|
+
}
|
|
20
|
+
// Those the store has, waited on again; one waited on past its time is let go.
|
|
21
|
+
resume() {
|
|
22
|
+
for (const [sid, w] of Object.entries(this.d.store.get().takeOvers ?? {})) {
|
|
23
|
+
if (Date.now() > w.until)
|
|
24
|
+
this.forget(sid);
|
|
25
|
+
else
|
|
26
|
+
this.wait({ kind: w.kind, cwd: w.cwd, resumeId: sid, title: w.title }, w.until);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async whenIdle(spec) {
|
|
30
|
+
if (this.waits.has(spec.resumeId))
|
|
31
|
+
return null;
|
|
32
|
+
if (stepDone(await this.onMacStatus(spec)))
|
|
33
|
+
return this.p.lifecycle.createSession(takenOver(spec));
|
|
34
|
+
const until = Date.now() + TAKE_OVER_WAIT_MS;
|
|
35
|
+
this.d.store.update((st) => {
|
|
36
|
+
st.takeOvers = { ...st.takeOvers, [spec.resumeId]: { kind: spec.kind, cwd: spec.cwd, title: spec.title, until } };
|
|
37
|
+
});
|
|
38
|
+
this.wait(spec, until);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
waiting(sid) {
|
|
42
|
+
return this.waits.has(sid);
|
|
43
|
+
}
|
|
44
|
+
cancel(sid) {
|
|
45
|
+
const done = this.waits.get(sid);
|
|
46
|
+
done?.();
|
|
47
|
+
return !!done;
|
|
48
|
+
}
|
|
49
|
+
wait(spec, until) {
|
|
50
|
+
const sid = spec.resumeId;
|
|
51
|
+
let stop;
|
|
52
|
+
const done = () => {
|
|
53
|
+
stop();
|
|
54
|
+
this.waits.delete(sid);
|
|
55
|
+
this.forget(sid);
|
|
56
|
+
this.s.emit();
|
|
57
|
+
};
|
|
58
|
+
const look = async () => {
|
|
59
|
+
const now = await this.onMacStatus(spec).catch(() => 'busy');
|
|
60
|
+
if (this.waits.get(sid) !== done)
|
|
61
|
+
return;
|
|
62
|
+
if (!stepDone(now)) {
|
|
63
|
+
if (Date.now() > until)
|
|
64
|
+
return done();
|
|
65
|
+
stop = this.s.later(TAKE_OVER_LOOK_MS, () => void look());
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
done();
|
|
69
|
+
await this.p.lifecycle.createSession(takenOver(spec)).catch((e) => console.error(`workoffdesk: conversation ${sid} could not be taken over from the Mac: ${e instanceof Error ? e.message : String(e)}`));
|
|
70
|
+
};
|
|
71
|
+
this.waits.set(sid, done);
|
|
72
|
+
stop = this.s.later(TAKE_OVER_LOOK_MS, () => void look());
|
|
73
|
+
this.s.emit();
|
|
74
|
+
}
|
|
75
|
+
forget(sid) {
|
|
76
|
+
if (!this.d.store.get().takeOvers?.[sid])
|
|
77
|
+
return;
|
|
78
|
+
this.d.store.update((st) => {
|
|
79
|
+
delete st.takeOvers?.[sid];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// What a conversation is doing in a terminal on the Mac: undefined when it is open in none.
|
|
83
|
+
async onMacStatus(spec) {
|
|
84
|
+
if (spec.kind === 'claude')
|
|
85
|
+
return (await this.p.lifecycle.openOnMac()).get(spec.resumeId)?.status;
|
|
86
|
+
if (!this.d.codexThreads)
|
|
87
|
+
throw new InvalidRequest('Codex is not set up on this Mac');
|
|
88
|
+
const t = await this.d.codexThreads.where(spec.resumeId);
|
|
89
|
+
return t?.holder?.at === 'terminal' ? (t.busy ? 'busy' : 'idle') : undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const takenOver = (spec) => spec.kind === 'claude' ? { ...spec, kind: 'claude', takeOver: true } : { ...spec, kind: 'codex', takeOver: true };
|
|
@@ -6,12 +6,15 @@ import { CodexThreads } from '../agents/codex/threads.js';
|
|
|
6
6
|
import { sqliteAvailable } from '../journal/db.js';
|
|
7
7
|
import { Journal } from '../journal/index.js';
|
|
8
8
|
import { Conflict, InvalidRequest } from '../system/errors.js';
|
|
9
|
+
import { projectOf } from '../system/project.js';
|
|
9
10
|
import { shownTitle } from '../system/store.js';
|
|
10
11
|
import { Accounts } from './machine/accounts.js';
|
|
11
12
|
import { Moves } from './machine/moves.js';
|
|
13
|
+
import { Naming } from './machine/naming.js';
|
|
14
|
+
import { TakeOvers } from './machine/takeOvers.js';
|
|
12
15
|
import { Input } from './machine/input.js';
|
|
13
16
|
import { Lifecycle } from './machine/lifecycle.js';
|
|
14
|
-
import { CHAT_SIZE, ID, MachineState, UUID, codexOf } from './machine/state.js';
|
|
17
|
+
import { CHAT_SIZE, ID, MachineState, UUID, codexOf, conversationOf } from './machine/state.js';
|
|
15
18
|
import { Status } from './machine/status.js';
|
|
16
19
|
import { Outbox, migrateQueueFile } from './outbox.js';
|
|
17
20
|
import { idOfTmuxName, tmuxName } from './sessionName.js';
|
|
@@ -35,6 +38,8 @@ export class LocalMachine {
|
|
|
35
38
|
// Per session, the last message still being taken in.
|
|
36
39
|
sending = new Map();
|
|
37
40
|
lastIds = '';
|
|
41
|
+
// When something last happened in each running session, as the last listing saw it.
|
|
42
|
+
seen = new Map();
|
|
38
43
|
constructor(deps) {
|
|
39
44
|
const claude = new ClaudeScreen(deps.tmux, CHAT_SIZE.cols);
|
|
40
45
|
if (!sqliteAvailable())
|
|
@@ -47,11 +52,11 @@ export class LocalMachine {
|
|
|
47
52
|
titled: (id, found, fresh) => this.p.lifecycle.conversationNamed(id, found, fresh),
|
|
48
53
|
});
|
|
49
54
|
migrateQueueFile(deps.queueFile ?? join(dirname(deps.store.file), 'queue.json'), this.journal);
|
|
55
|
+
const log = deps.journal?.log ?? (() => { });
|
|
50
56
|
const outbox = new Outbox({
|
|
51
57
|
store: this.journal,
|
|
52
58
|
status: (id) => this.getStatus(id),
|
|
53
|
-
// Claude is at work on it from
|
|
54
|
-
// hook says it started.
|
|
59
|
+
// Claude is at work on it from when it goes in, the next waiting until it is done, not on a hook.
|
|
55
60
|
deliver: async (id, text, qid) => {
|
|
56
61
|
if (deps.store.get().sessions[id]?.kind === 'codex')
|
|
57
62
|
return codexOf(this.d).send(id, text, qid);
|
|
@@ -65,15 +70,18 @@ export class LocalMachine {
|
|
|
65
70
|
this.setStatus(id, 'working');
|
|
66
71
|
},
|
|
67
72
|
changed: () => this.s.emit(),
|
|
73
|
+
held: (id, qid, why) => log('message held', { session: id, qid, why: why instanceof Error ? why.constructor.name : why }),
|
|
68
74
|
failed: (id, e) => e instanceof DialogOpen || e instanceof AgentQuit || console.error(`workoffdesk: a message to session ${id} could not go and waits: ${e instanceof Error ? e.message : String(e)}`),
|
|
69
75
|
});
|
|
70
76
|
const shell = deps.shell ?? process.env.SHELL ?? '/bin/zsh';
|
|
71
|
-
const log = deps.journal?.log ?? (() => { });
|
|
72
77
|
const codex = deps.codex
|
|
73
78
|
? new CodexAgent({ tmux: deps.tmux, sock: deps.codex.sock, codexBin: deps.codex.bin, shell }, {
|
|
74
79
|
journal: this.journal,
|
|
75
80
|
sessions: () => Object.entries(deps.store.get().sessions).flatMap(([id, m]) => (m.kind === 'codex' ? [{ id, cwd: m.cwd, createdAt: m.createdAt, threadId: m.codexThreadId }] : [])),
|
|
76
|
-
setThread: (id, threadId) =>
|
|
81
|
+
setThread: (id, threadId) => {
|
|
82
|
+
deps.store.update((st) => void (st.sessions[id] && (st.sessions[id].codexThreadId = threadId)));
|
|
83
|
+
this.p.naming.watch();
|
|
84
|
+
},
|
|
77
85
|
moved: (id, status, said) => this.p.status.moved(id, status, said),
|
|
78
86
|
named: (id, name) => this.p.lifecycle.conversationNamed(id, { ai: name }, false),
|
|
79
87
|
setUsage: (id, usage) => this.setUsage(id, usage),
|
|
@@ -107,11 +115,17 @@ export class LocalMachine {
|
|
|
107
115
|
p.status = new Status(this.s, this.d, p);
|
|
108
116
|
p.accounts = new Accounts(this.d, p);
|
|
109
117
|
p.moves = new Moves(this.s, this.d, p);
|
|
118
|
+
p.naming = new Naming(this.s, this.d, p);
|
|
119
|
+
p.takeOvers = new TakeOvers(this.s, this.d, p);
|
|
110
120
|
this.p = p;
|
|
121
|
+
p.naming.watch();
|
|
122
|
+
p.takeOvers.resume();
|
|
111
123
|
}
|
|
112
|
-
async
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
codexOnMac = async () => (this.d.codexThreads ? this.d.codexThreads.list() : []);
|
|
125
|
+
// Every Codex thread people opened on the Mac, however old.
|
|
126
|
+
allCodexOnMac = async () => (this.d.codexThreads ? this.d.codexThreads.all() : []);
|
|
127
|
+
// One Codex thread, however old; null when there is none.
|
|
128
|
+
oneCodexOnMac = async (id) => (this.d.codexThreads ? this.d.codexThreads.where(id).catch(() => null) : null);
|
|
115
129
|
codexChat(threadId) {
|
|
116
130
|
if (!UUID.test(threadId))
|
|
117
131
|
throw new InvalidRequest('thread id must be a uuid');
|
|
@@ -157,10 +171,8 @@ export class LocalMachine {
|
|
|
157
171
|
});
|
|
158
172
|
const liveIds = new Set(live.map((t) => t.id));
|
|
159
173
|
const stale = known.filter((id) => !liveIds.has(id));
|
|
160
|
-
if (stale.length)
|
|
161
|
-
this.
|
|
162
|
-
stale.forEach((id) => this.p.status.forget(id));
|
|
163
|
-
}
|
|
174
|
+
if (stale.length)
|
|
175
|
+
this.leave(stale);
|
|
164
176
|
const sessions = live.map(({ id, ...t }) => {
|
|
165
177
|
const meta = this.d.store.get().sessions[id] ?? {
|
|
166
178
|
title: t.name,
|
|
@@ -179,12 +191,50 @@ export class LocalMachine {
|
|
|
179
191
|
for (const s of sessions)
|
|
180
192
|
if (s.meta.kind === 'codex')
|
|
181
193
|
this.p.status.lookCodex(s.id, s.meta);
|
|
182
|
-
const
|
|
194
|
+
for (const s of sessions)
|
|
195
|
+
this.seen.set(s.id, s.activity);
|
|
196
|
+
const past = this.d.store.get().past;
|
|
197
|
+
const ids = `${[...liveIds].sort().join(',')}|${Object.keys(past).sort().join(',')}`;
|
|
183
198
|
if (ids !== this.lastIds) {
|
|
184
199
|
this.lastIds = ids;
|
|
185
200
|
this.s.emit();
|
|
186
201
|
}
|
|
187
|
-
|
|
202
|
+
const running = sessions.map((s) => this.toSession(s.id, s.meta, s.activity));
|
|
203
|
+
const held = new Set(running.flatMap((s) => conversationOf(s) ?? []));
|
|
204
|
+
const kept = Object.entries(past).flatMap(([id, p]) => (held.has(conversationOf(p) ?? '') ? [] : [this.toPast(id, p)]));
|
|
205
|
+
return [...running, ...kept].sort((a, b) => b.lastActivity - a.lastActivity);
|
|
206
|
+
}
|
|
207
|
+
// Sessions tmux no longer has. A Claude or Codex one is kept as past, with its journal, unless a past one of
|
|
208
|
+
// the same conversation has more of it; a shell one is dropped.
|
|
209
|
+
leave(ids) {
|
|
210
|
+
const dropped = [];
|
|
211
|
+
this.d.store.update((st) => {
|
|
212
|
+
for (const id of ids) {
|
|
213
|
+
const meta = st.sessions[id];
|
|
214
|
+
delete st.sessions[id];
|
|
215
|
+
const conv = meta && conversationOf(meta);
|
|
216
|
+
const other = conv ? Object.keys(st.past).find((p) => conversationOf(st.past[p]) === conv) : undefined;
|
|
217
|
+
if (!meta || meta.kind === 'shell' || (other && this.journal.itemCount(other) > this.journal.itemCount(id))) {
|
|
218
|
+
dropped.push(id);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (other) {
|
|
222
|
+
delete st.past[other];
|
|
223
|
+
dropped.push(other);
|
|
224
|
+
}
|
|
225
|
+
st.past[id] = { ...meta, lastActivity: this.seen.get(id) ?? meta.createdAt };
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
for (const id of ids) {
|
|
229
|
+
this.p.status.forget(id);
|
|
230
|
+
this.seen.delete(id);
|
|
231
|
+
}
|
|
232
|
+
for (const id of dropped)
|
|
233
|
+
this.journal.drop(id);
|
|
234
|
+
}
|
|
235
|
+
// Gone from tmux and kept to read.
|
|
236
|
+
isPast(id) {
|
|
237
|
+
return Object.hasOwn(this.d.store.get().past, id);
|
|
188
238
|
}
|
|
189
239
|
async exists(id) {
|
|
190
240
|
return ID.test(id) && this.d.tmux.hasSession(tmuxName(id));
|
|
@@ -209,9 +259,7 @@ export class LocalMachine {
|
|
|
209
259
|
});
|
|
210
260
|
return taken;
|
|
211
261
|
}
|
|
212
|
-
unsend(id, queuedId) {
|
|
213
|
-
return this.d.outbox.remove(id, queuedId);
|
|
214
|
-
}
|
|
262
|
+
unsend(id, queuedId) { return this.d.outbox.remove(id, queuedId); }
|
|
215
263
|
async sendNow(id, queuedId) {
|
|
216
264
|
if (!ID.test(id))
|
|
217
265
|
throw new InvalidRequest('bad session id');
|
|
@@ -219,17 +267,18 @@ export class LocalMachine {
|
|
|
219
267
|
throw new Conflict('a dialog is open; pick an option or press Esc first', 'dialog-open');
|
|
220
268
|
return this.d.outbox.sendNow(id, queuedId);
|
|
221
269
|
}
|
|
222
|
-
moveQueued(id, queuedId, by) {
|
|
223
|
-
return this.d.outbox.move(id, queuedId, by);
|
|
224
|
-
}
|
|
270
|
+
moveQueued(id, queuedId, by) { return this.d.outbox.move(id, queuedId, by); }
|
|
225
271
|
createSession(spec) { return this.p.lifecycle.createSession(spec); }
|
|
226
272
|
continueSession(id) { return this.p.lifecycle.continueSession(id); }
|
|
227
|
-
openOnMac() { return this.p.lifecycle.openOnMac(); }
|
|
228
|
-
|
|
273
|
+
openOnMac(recentMs) { return this.p.lifecycle.openOnMac(recentMs); }
|
|
274
|
+
macClaudeOf(pid) { return this.p.lifecycle.macClaudeOf(pid); }
|
|
275
|
+
quitOnMac(id, force) { return this.p.lifecycle.quitOthersOnMac(id, force); }
|
|
276
|
+
takeOverWhenIdle(spec) { return this.p.takeOvers.whenIdle(spec); }
|
|
229
277
|
takingOver(claudeSessionId) { return this.p.lifecycle.takingOver(claudeSessionId); }
|
|
230
|
-
cancelTakeOver(claudeSessionId) { return this.p.
|
|
278
|
+
cancelTakeOver(claudeSessionId) { return this.p.takeOvers.cancel(claudeSessionId); }
|
|
231
279
|
killSession(id) { return this.p.lifecycle.killSession(id); }
|
|
232
280
|
renameSession(id, title) { return this.p.lifecycle.renameSession(id, title); }
|
|
281
|
+
pinSession(id, on) { return this.p.lifecycle.pinSession(id, on); }
|
|
233
282
|
claudeName(claudeSessionId) { return this.p.lifecycle.claudeName(claudeSessionId); }
|
|
234
283
|
choices(id) { return this.p.input.choices(id); }
|
|
235
284
|
onScreen(id) { return this.p.input.onScreen(id); }
|
|
@@ -241,6 +290,7 @@ export class LocalMachine {
|
|
|
241
290
|
type(id, text) { return this.p.input.type(id, text); }
|
|
242
291
|
close(id) { return this.p.input.close(id); }
|
|
243
292
|
press(id, keys) { return this.p.input.press(id, keys); }
|
|
293
|
+
toBackground(id) { return this.p.input.toBackground(id); }
|
|
244
294
|
runCommand(id, command) { return this.p.input.runCommand(id, command); }
|
|
245
295
|
fitForChat(id) { return this.p.input.fitForChat(id); }
|
|
246
296
|
getStatus(id) { return this.p.status.getStatus(id); }
|
|
@@ -252,6 +302,7 @@ export class LocalMachine {
|
|
|
252
302
|
setClaudeSessionId(id, claudeSessionId) { this.p.status.setClaudeSessionId(id, claudeSessionId); }
|
|
253
303
|
claudeAccountsView() { return this.p.accounts.claudeAccountsView(); }
|
|
254
304
|
pickClaudeAccount(id) { return this.p.accounts.pickClaudeAccount(id); }
|
|
305
|
+
moveThis(id, when) { return this.p.moves.moveThis(id, when); }
|
|
255
306
|
async moveToAccountInUse() {
|
|
256
307
|
await this.p.moves.moveAll();
|
|
257
308
|
return this.p.accounts.claudeAccountsView();
|
|
@@ -259,6 +310,13 @@ export class LocalMachine {
|
|
|
259
310
|
sessionMeta(id) {
|
|
260
311
|
return this.d.store.get().sessions[id];
|
|
261
312
|
}
|
|
313
|
+
// How many sessions are running a turn or compacting.
|
|
314
|
+
working() {
|
|
315
|
+
return Object.keys(this.d.store.get().sessions).filter((id) => {
|
|
316
|
+
const status = this.getStatus(id);
|
|
317
|
+
return status === 'working' || status === 'compacting';
|
|
318
|
+
}).length;
|
|
319
|
+
}
|
|
262
320
|
onChange(cb) {
|
|
263
321
|
this.s.listeners.add(cb);
|
|
264
322
|
return () => this.s.listeners.delete(cb);
|
|
@@ -282,7 +340,12 @@ export class LocalMachine {
|
|
|
282
340
|
clearTimeout(timer);
|
|
283
341
|
};
|
|
284
342
|
}
|
|
343
|
+
toPast(id, p) {
|
|
344
|
+
const { lastActivity, ...meta } = p;
|
|
345
|
+
return { ...this.toSession(id, meta, lastActivity), status: 'ended', usage: null, claudeAccount: undefined, past: true };
|
|
346
|
+
}
|
|
285
347
|
toSession(id, meta, lastActivity) {
|
|
348
|
+
const project = projectOf(meta.cwd);
|
|
286
349
|
return {
|
|
287
350
|
id,
|
|
288
351
|
title: shownTitle(meta),
|
|
@@ -295,6 +358,7 @@ export class LocalMachine {
|
|
|
295
358
|
status: meta.kind === 'shell' ? null : (this.s.statuses.get(id) ?? 'idle'),
|
|
296
359
|
usage: this.s.usages.get(id) ?? null,
|
|
297
360
|
claudeAccount: this.p.accounts.accountLabel(meta),
|
|
361
|
+
...(project && { project }), ...(meta.pinned && { pinned: meta.pinned }),
|
|
298
362
|
};
|
|
299
363
|
}
|
|
300
364
|
}
|
|
@@ -38,6 +38,7 @@ export class Outbox {
|
|
|
38
38
|
deliver;
|
|
39
39
|
changed;
|
|
40
40
|
failed;
|
|
41
|
+
held;
|
|
41
42
|
gapMs;
|
|
42
43
|
running = new Map();
|
|
43
44
|
again = new Set();
|
|
@@ -49,6 +50,7 @@ export class Outbox {
|
|
|
49
50
|
this.deliver = deps.deliver;
|
|
50
51
|
this.changed = deps.changed;
|
|
51
52
|
this.failed = deps.failed ?? (() => { });
|
|
53
|
+
this.held = deps.held ?? (() => { });
|
|
52
54
|
this.gapMs = deps.gapMs ?? GAP_MS;
|
|
53
55
|
}
|
|
54
56
|
list(id) {
|
|
@@ -67,9 +69,11 @@ export class Outbox {
|
|
|
67
69
|
if (known && known.state !== 'taken')
|
|
68
70
|
return null;
|
|
69
71
|
const m = { id: qid, text, sentAt: Date.now(), state: 'queued', ord: null, item: null };
|
|
70
|
-
|
|
72
|
+
const status = this.status(id);
|
|
73
|
+
if (!this.waiting(id).length && !HOLDS.has(status)) {
|
|
71
74
|
return this.pasting(qid, async () => ((await this.paste(id, m)) ? null : (this.put(id, { ...m, ord: this.lastOrd(id) + 1 }), queued(m))));
|
|
72
75
|
}
|
|
76
|
+
this.held(id, qid, HOLDS.has(status) ? status : 'behind');
|
|
73
77
|
this.put(id, { ...m, ord: this.lastOrd(id) + 1 });
|
|
74
78
|
void this.flush(id);
|
|
75
79
|
return queued(m);
|
|
@@ -163,6 +167,8 @@ export class Outbox {
|
|
|
163
167
|
catch (e) {
|
|
164
168
|
this.failed(id, e);
|
|
165
169
|
went = mayHaveRun(e);
|
|
170
|
+
if (!went)
|
|
171
|
+
this.held(id, m.id, e);
|
|
166
172
|
}
|
|
167
173
|
// The transcript may have shown it landed before the paste answered.
|
|
168
174
|
if (went && this.store.messages(id).find((r) => r.id === m.id)?.state === 'delivering')
|
|
@@ -38,6 +38,10 @@ export function parseConfig(raw) {
|
|
|
38
38
|
if (typeof rpId !== 'string' || !(url.hostname === rpId || url.hostname.endsWith(`.${rpId}`))) {
|
|
39
39
|
throw new ConfigError('rpId', m.config.rpId(url.hostname));
|
|
40
40
|
}
|
|
41
|
+
const extra = r.extraOrigins ?? [];
|
|
42
|
+
if (!isStrings(extra) || !extra.every((o) => originUrl(o)?.protocol === 'https:'))
|
|
43
|
+
throw new ConfigError('extraOrigins', m.config.extraOrigins);
|
|
44
|
+
const extraOrigins = [...new Set(extra)].filter((o) => o !== url.origin);
|
|
41
45
|
const pinnedDirs = r.pinnedDirs ?? [];
|
|
42
46
|
if (!isStrings(pinnedDirs))
|
|
43
47
|
throw new ConfigError('pinnedDirs', m.config.pinnedDirs);
|
|
@@ -51,7 +55,18 @@ export function parseConfig(raw) {
|
|
|
51
55
|
const preventSleep = r.preventSleep ?? true;
|
|
52
56
|
if (typeof preventSleep !== 'boolean')
|
|
53
57
|
throw new ConfigError('preventSleep', m.config.preventSleep);
|
|
54
|
-
return { port, origin: url.origin, rpId, pinnedDirs, serviceLabel, claudeArgs, preventSleep };
|
|
58
|
+
return { port, origin: url.origin, extraOrigins, rpId, pinnedDirs, serviceLabel, claudeArgs, preventSleep };
|
|
59
|
+
}
|
|
60
|
+
// The origin first. Another address shares the rpId when its host is under it; else its passkeys are its own.
|
|
61
|
+
export function sites(c) {
|
|
62
|
+
const under = (host) => host === c.rpId || host.endsWith(`.${c.rpId}`);
|
|
63
|
+
return [
|
|
64
|
+
{ origin: c.origin, rpId: c.rpId },
|
|
65
|
+
...c.extraOrigins.map((origin) => {
|
|
66
|
+
const host = new URL(origin).hostname;
|
|
67
|
+
return { origin, rpId: under(host) ? c.rpId : host };
|
|
68
|
+
}),
|
|
69
|
+
];
|
|
55
70
|
}
|
|
56
71
|
export function loadConfig(home) {
|
|
57
72
|
const file = join(home, 'config.json');
|
|
@@ -16,8 +16,8 @@ export class InvalidRequest extends DomainError {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
export class NotFound extends DomainError {
|
|
19
|
-
constructor(message) {
|
|
20
|
-
super(404, message);
|
|
19
|
+
constructor(message, code) {
|
|
20
|
+
super(404, message, code);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
export class Conflict extends DomainError {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// What the CLI says to the person at the Mac: Chinese when the Mac speaks it, English otherwise.
|
|
2
2
|
// The phone has its own dictionary (@workoffdesk/i18n); errors the server sends it go by code, not words.
|
|
3
|
+
import { zh } from './messages.zh.js';
|
|
4
|
+
export { zh };
|
|
3
5
|
// The same rule as the phone's.
|
|
4
6
|
export function pickLang(languages) {
|
|
5
7
|
for (const l of languages) {
|
|
@@ -35,6 +37,7 @@ export const en = {
|
|
|
35
37
|
restart: [['restart', 'Restart the service; sessions keep running']],
|
|
36
38
|
doctor: [['doctor', 'Check everything and print the fix for each failure']],
|
|
37
39
|
status: [['status --json', 'The checks, the version, the releases and the newest errors, as JSON']],
|
|
40
|
+
activity: [['activity --json', 'How many sessions are working: a line of JSON now and at each change']],
|
|
38
41
|
claude: [['claude [--resume <session id>]', 'Start Claude in this folder as a session and join it']],
|
|
39
42
|
codex: [['codex [--resume <thread id>]', 'Start Codex in this folder as a session and join it']],
|
|
40
43
|
attach: [['attach [id]', 'Join a session from this terminal; without an id, pick one']],
|
|
@@ -50,6 +53,7 @@ export const en = {
|
|
|
50
53
|
'--help': [['--help', 'Print this list']],
|
|
51
54
|
},
|
|
52
55
|
statusUsage: 'Usage: workoffdesk status --json (what the menu bar app reads; doctor is the one for people)',
|
|
56
|
+
activityUsage: 'Usage: workoffdesk activity --json (what the menu bar app reads)',
|
|
53
57
|
notConfigured: (example) => `Not configured yet. Tell setup the address the site is reached at:\n workoffdesk setup --origin ${example}`,
|
|
54
58
|
askOrigin: (example) => `The address the site is reached at (the one the phone opens, like ${example}): `,
|
|
55
59
|
askUsername: 'Username: ',
|
|
@@ -116,7 +120,7 @@ To restart the service only: launchctl kickstart -k gui/$(id -u)/${o.label}`,
|
|
|
116
120
|
sleepsOff: 'preventSleep is false: when the Mac idle-sleeps, the phone cannot reach it until someone wakes it',
|
|
117
121
|
sleepFix: (file) => `Remove "preventSleep": false from ${file}, then restart the service.`,
|
|
118
122
|
awake: 'the Mac does not idle-sleep while the service runs',
|
|
119
|
-
noApns:
|
|
123
|
+
noApns: "no push key: an iPhone app you build yourself gets no notifications; the App Store app's come through its relay, and web push works without it",
|
|
120
124
|
apnsKey: (keyId) => `push key ${keyId}`,
|
|
121
125
|
apnsFix: (file) => `Fix ${file}, or remove it if you do not use the iPhone app.`,
|
|
122
126
|
failed: 'Some checks failed; each ✗ says what to do.',
|
|
@@ -150,6 +154,7 @@ To restart the service only: launchctl kickstart -k gui/$(id -u)/${o.label}`,
|
|
|
150
154
|
pinnedDirs: 'an array of folder paths',
|
|
151
155
|
serviceLabel: 'a launchd label of letters, digits, dots and hyphens',
|
|
152
156
|
claudeArgs: 'an array of arguments claude is started with, like ["--chrome"]',
|
|
157
|
+
extraOrigins: 'an array of other https addresses the site is also reached at, like ["https://other.example.com"]',
|
|
153
158
|
preventSleep: 'true (keep the Mac from idle-sleeping while the service runs) or false',
|
|
154
159
|
missing: (file) => `set, but ${file} does not exist yet; run workoffdesk setup first`,
|
|
155
160
|
format: 'the format',
|
|
@@ -188,6 +193,7 @@ To restart the service only: launchctl kickstart -k gui/$(id -u)/${o.label}`,
|
|
|
188
193
|
},
|
|
189
194
|
takeOver: {
|
|
190
195
|
openOnMac: (ttys, agent) => `this conversation is open in a terminal on the Mac${ttys.some(Boolean) ? ` (${ttys.filter(Boolean).join(', ')})` : ''}; quit ${agent} there first, or take it over from the phone`,
|
|
196
|
+
macUnanswered: 'The Mac did not say in time whether this conversation is open in a terminal there; try again in a moment',
|
|
191
197
|
wouldNotQuit: (agent) => `${agent} in the terminal on the Mac did not quit within 10 seconds; nothing was resumed`,
|
|
192
198
|
openElsewhere: 'this conversation is open in another Codex app (an editor or the Codex app); close it there first',
|
|
193
199
|
// Said in the terminal it was quit in.
|
|
@@ -196,183 +202,15 @@ To restart the service only: launchctl kickstart -k gui/$(id -u)/${o.label}`,
|
|
|
196
202
|
goOn: {
|
|
197
203
|
onlyClaude: 'only a Claude session whose Claude quit is gone on with in place',
|
|
198
204
|
terminalBusy: "something runs in this session's terminal on the Mac; quit it there first, or start another conversation",
|
|
205
|
+
conversationGone: (agent) => `${agent} no longer keeps this conversation, so it cannot be gone on with; what was said can still be read here`,
|
|
199
206
|
},
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
unknownCommand: (cmd) => `没有 ${cmd} 这个命令。`,
|
|
206
|
-
help: {
|
|
207
|
-
setup: [
|
|
208
|
-
['setup', '设用户名和密码;再跑一次会换新密码,所有设备都要重新登录'],
|
|
209
|
-
['setup --web', '在浏览器里做设置,包括地址和服务'],
|
|
210
|
-
],
|
|
211
|
-
install: [['install [--tunnel <名字>]', '写好 launchd 服务(不启动),并打印接下来要跑的命令']],
|
|
212
|
-
start: [['start', '启动服务,等它应答']],
|
|
213
|
-
stop: [['stop', '停掉服务,直到下次登录或 start']],
|
|
214
|
-
restart: [['restart', '重启服务;会话继续跑']],
|
|
215
|
-
doctor: [['doctor', '检查所有项,每项没过都给出修复方法']],
|
|
216
|
-
status: [['status --json', '以 JSON 输出检查结果、版本、保留的各版本和最近的错误']],
|
|
217
|
-
claude: [['claude [--resume <会话 id>]', '在当前目录开一个 Claude 会话并接上']],
|
|
218
|
-
codex: [['codex [--resume <对话 id>]', '在当前目录开一个 Codex 会话并接上']],
|
|
219
|
-
attach: [['attach [id]', '在当前终端接上一个会话;不给 id 就让你挑']],
|
|
220
|
-
account: [
|
|
221
|
-
['account [--json]', '这台 Mac 上的 Claude 账号,以及新会话用哪一个'],
|
|
222
|
-
['account use <邮箱或目录> [--move]', '新会话改用它;加 --move 把开着的会话也搬过去'],
|
|
223
|
-
['account move', '把开着的会话搬到正在用的账号,每个等它这轮回复完再搬'],
|
|
224
|
-
],
|
|
225
|
-
serve: [['serve', '在当前终端里跑服务器,而不是作为后台服务']],
|
|
226
|
-
deploy: [['deploy <构建目录>', '把一个构建装成新版本并切过去']],
|
|
227
|
-
rollback: [['rollback', '回到上一个版本']],
|
|
228
|
-
'--version': [['--version', '打印装的版本']],
|
|
229
|
-
'--help': [['--help', '打印这张列表']],
|
|
230
|
-
},
|
|
231
|
-
statusUsage: '用法:workoffdesk status --json(给菜单栏 App 读的;给人看用 doctor)',
|
|
232
|
-
notConfigured: (example) => `还没有配置。告诉 setup 这个站点的访问地址:\n workoffdesk setup --origin ${example}`,
|
|
233
|
-
askOrigin: (example) => `站点的访问地址(手机打开的那个,比如 ${example}):`,
|
|
234
|
-
askUsername: '用户名:',
|
|
235
|
-
emptyUsername: '用户名不能为空',
|
|
236
|
-
askPassword: (min) => `密码(至少 ${min} 个字符):`,
|
|
237
|
-
askAgain: '再输一次:',
|
|
238
|
-
passwordsDiffer: '两次输入的密码不一致',
|
|
239
|
-
passwordTooShort: (min) => `密码至少要 ${min} 个字符`,
|
|
240
|
-
setUp: (username, origin, configFile, port) => `用户 ${username} 已设置好,地址 ${origin}。配置:${configFile}
|
|
241
|
-
|
|
242
|
-
下一步:
|
|
243
|
-
workoffdesk serve 先在本机试一下(http://127.0.0.1:${port})
|
|
244
|
-
workoffdesk install 装成登录时自动启动的服务,并看看怎么在前面接隧道或反向代理
|
|
245
|
-
开放到公网之前,先读 SECURITY.md:谁进得了这个站点,谁就拿到了这台电脑的终端。`,
|
|
246
|
-
notSetUp: '还没有设置用户。运行:workoffdesk setup',
|
|
247
|
-
scan: (origin) => `用手机相机扫一下,打开 ${origin}:`,
|
|
248
|
-
running: (port, origin) => `workoffdesk 正在 http://127.0.0.1:${port} 上运行,访问地址 ${origin}`,
|
|
249
|
-
noSessions: '没有会话。',
|
|
250
|
-
whichOne: '选哪一个?',
|
|
251
|
-
noSuchNumber: '没有这个编号',
|
|
252
|
-
notRunning: 'WorkOffDesk 服务没有在运行。先启动它(workoffdesk serve,或已安装的服务)。',
|
|
253
|
-
couldNotOpen: (why) => `打不开会话:${why}`,
|
|
254
|
-
noTunnel: (name) => `没有叫 ${name} 的隧道。运行:\n cloudflared tunnel create ${name}\n然后再运行一次 workoffdesk install --tunnel ${name}。`,
|
|
255
|
-
noCloudflared: '找不到 cloudflared。先运行 brew install cloudflared。',
|
|
256
|
-
notBuilt: (cliPath) => `注意:${cliPath} 还不存在,先构建(pnpm build)。`,
|
|
257
|
-
noCredentials: (file) => `注意:隧道凭据 ${file} 不在这台电脑上;请在创建这条隧道的电脑上运行它。`,
|
|
258
|
-
frontDoor: (port, origin) => `服务只监听 http://127.0.0.1:${port}。需要在它前面放一个能把 ${origin} 转到这里的东西:
|
|
259
|
-
Cloudflare 隧道:cloudflared tunnel create <名字>,然后 workoffdesk install --tunnel <名字>
|
|
260
|
-
或任何反向代理(Caddy、nginx…),把 ${origin} 转发到 http://127.0.0.1:${port},并支持 WebSocket`,
|
|
261
|
-
installed: (o) => `node:${o.nodePath}(换了 node 版本之后,用 --node <路径> 再运行一次 install)
|
|
262
|
-
已写入:
|
|
263
|
-
${o.written.map((f) => ` ${f}`).join('\n')}
|
|
264
|
-
${o.warnings.length ? `\n${o.warnings.join('\n')}\n` : ''}
|
|
265
|
-
现在要运行的命令(只需第一次):
|
|
266
|
-
${o.frontDoor}
|
|
267
|
-
|
|
268
|
-
以后更新:workoffdesk deploy <构建目录>;需要时 workoffdesk rollback。
|
|
269
|
-
只重启服务:launchctl kickstart -k gui/$(id -u)/${o.label}`,
|
|
270
|
-
deployUsage: '用法:workoffdesk deploy <构建目录> [--new-app-runtime](构建目录是 pnpm deploy --prod 生成的那个)',
|
|
271
|
-
},
|
|
272
|
-
doctor: {
|
|
273
|
-
notFound: '找不到',
|
|
274
|
-
unknownVersion: (version) => `看不出版本(${version})`,
|
|
275
|
-
tooOld: (version, min) => `${version};需要 ${min} 或更高`,
|
|
276
|
-
macosOnly: '服务只支持 macOS(它用 launchd,管的是这台 Mac 上的 Claude Code 会话)。',
|
|
277
|
-
nodeFix: (min) => `装 node ${min} 或更高版本(brew install node,或 nvm install 22),然后再运行一次 workoffdesk install。`,
|
|
278
|
-
tmuxFix: 'brew install tmux',
|
|
279
|
-
claudeFix: '安装 Claude Code:curl -fsSL https://claude.ai/install.sh | bash',
|
|
280
|
-
noCodex: '没有安装(可选:用来开 Codex 会话)',
|
|
281
|
-
setupFix: 'workoffdesk setup',
|
|
282
|
-
setUp: (user, origin) => `用户 ${user},地址 ${origin}`,
|
|
283
|
-
notSetUp: (home) => `还没有设置(${home} 里没有 config.json)`,
|
|
284
|
-
noUser: '还没有设置用户',
|
|
285
|
-
running: (local, version) => `在 ${local} 上运行${version ? `,版本 ${version}` : ''}`,
|
|
286
|
-
notAnswering: (local) => `${local} 上没有回应`,
|
|
287
|
-
otherVersion: (local, running, mine) => `${local} 上跑的是 ${running},这个命令是 ${mine}`,
|
|
288
|
-
restartFix: (label) => `重启它:launchctl kickstart -k gui/$(id -u)/${label}`,
|
|
289
|
-
installFix: '把它装成服务:workoffdesk install,再执行它打印的那行 launchctl bootstrap(或者用 workoffdesk serve 先试)',
|
|
290
|
-
bootstrapFix: (plist) => `启动它:launchctl bootstrap gui/$(id -u) ${plist}`,
|
|
291
|
-
kickstartFix: (label, log) => `重启它:launchctl kickstart -k gui/$(id -u)/${label};如果又停了,原因在 ${log}`,
|
|
292
|
-
localOnly: (origin) => `${origin}:只有这台 Mac 能访问`,
|
|
293
|
-
notReached: (origin) => `${origin} 到不了服务`,
|
|
294
|
-
originFix: (origin, local) => `检查把 ${origin} 转到 ${local} 的隧道或反向代理。`,
|
|
295
|
-
sleepsOff: 'preventSleep 是 false:Mac 空闲睡眠之后,手机就连不上,直到有人把它叫醒',
|
|
296
|
-
sleepFix: (file) => `把 ${file} 里的 "preventSleep": false 删掉,然后重启服务。`,
|
|
297
|
-
awake: '服务运行时 Mac 不会空闲睡眠',
|
|
298
|
-
noApns: '没有推送密钥:iPhone App 收不到通知;网页推送不受影响',
|
|
299
|
-
apnsKey: (keyId) => `推送密钥 ${keyId}`,
|
|
300
|
-
apnsFix: (file) => `改好 ${file};不用 iPhone App 的话删掉它。`,
|
|
301
|
-
failed: '有检查没通过;每个 ✗ 下面写了怎么办。',
|
|
207
|
+
moveThis: {
|
|
208
|
+
cannot: 'only a Claude session here whose Claude runs, in a folder still there, moves to another account',
|
|
209
|
+
busy: 'it moves once Claude is at its prompt with nothing waiting to go in; try again when its reply is done',
|
|
210
|
+
macBusy: 'this conversation is also open in a terminal on the Mac, where Claude is at work or asking something; let it finish there first',
|
|
211
|
+
failed: (why) => `it stays on its account: ${why}`,
|
|
302
212
|
},
|
|
303
|
-
|
|
304
|
-
started: (port) => `服务已在 http://127.0.0.1:${port} 运行`,
|
|
305
|
-
stopped: '服务已停止;下次登录时会再启动,或运行 workoffdesk start',
|
|
306
|
-
restarted: '服务已重启',
|
|
307
|
-
notUp: (log) => `服务 30 秒内没有应答;原因多半在 ${log}`,
|
|
308
|
-
},
|
|
309
|
-
wizard: {
|
|
310
|
-
open: (url) => `接下来在浏览器里设置。如果没有自动打开,在这台 Mac 上打开这个地址:\n ${url}\n按 Ctrl-C 结束。`,
|
|
311
|
-
idle: '设置页 30 分钟没人用,已经结束。再运行 workoffdesk setup --web 重新开始。',
|
|
312
|
-
done: '设置完成。',
|
|
313
|
-
notReady: '先设好账号和地址',
|
|
314
|
-
noTailscale: '这台 Mac 上的 Tailscale 没有运行',
|
|
315
|
-
tailscaleFailed: (out) => `tailscale serve 失败:${out}`,
|
|
316
|
-
noTunnel: (name) => `这台 Mac 上没有叫 ${name} 的 Cloudflare 隧道`,
|
|
317
|
-
badHostname: '主机名是 workoffdesk.example.com 这样的名字,不带 https:// 也不带路径',
|
|
318
|
-
routeFailed: (out) => `cloudflared 没能把主机名指到隧道:${out}`,
|
|
319
|
-
},
|
|
320
|
-
node: {
|
|
321
|
-
doesNotRun: (node) => `${node} 运行不了。`,
|
|
322
|
-
tooOld: (node, version, min) => `${node} 是 ${version};WorkOffDesk 需要 node ${min} 或更高版本。`,
|
|
323
|
-
},
|
|
324
|
-
config: {
|
|
325
|
-
wrong: (field, expected) => `config.json:${field} 不对,应该是${expected}`,
|
|
326
|
-
origin: '站点的访问地址,比如 https://workoffdesk.example.com(在本机试用就用 http://localhost:<端口>)',
|
|
327
|
-
port: '1 到 65535 之间的整数',
|
|
328
|
-
rpId: (hostname) => `站点的主机名(${hostname})或它的上级域名`,
|
|
329
|
-
pinnedDirs: '文件夹路径的数组',
|
|
330
|
-
serviceLabel: '由字母、数字、点和连字符组成的 launchd 标签',
|
|
331
|
-
claudeArgs: '启动 claude 时带的参数数组,比如 ["--chrome"]',
|
|
332
|
-
preventSleep: 'true(服务运行时不让 Mac 空闲睡眠)或 false',
|
|
333
|
-
missing: (file) => `已设置的,但 ${file} 还不存在;先运行 workoffdesk setup`,
|
|
334
|
-
format: '格式',
|
|
335
|
-
notJson: (why) => `合法的 JSON(${why})`,
|
|
336
|
-
},
|
|
337
|
-
release: {
|
|
338
|
-
notABuild: (dir) => `${dir} 不是一个构建(需要有 package.json 和 dist/server/cli.js)`,
|
|
339
|
-
smokeFailed: (version) => `${version} 没通过冒烟测试;正在运行的版本没动`,
|
|
340
|
-
didNotComeUp: (version, prev) => `${version} 30 秒内没有起来;${prev ? `已切回 ${prev}` : '没有更早的版本可以切回'}`,
|
|
341
|
-
staleBuild: (version, built) => `这个构建写的是 ${version},但它的 dist ${built ? `是为 ${built} 构建的` : '不是一次完整构建产出的'};重新运行 pnpm build`,
|
|
342
|
-
deployed: (version, name) => `已部署 ${version}(${name})`,
|
|
343
|
-
builtFrom: (s) => (s.commit ? `构建自 ${s.commit.slice(0, 12)}${s.dirty ? ',带未提交的改动' : ''}` : '不是在 git 里构建的'),
|
|
344
|
-
appUpdate: (runtime) => (runtime ? `iPhone 应用更新,运行时 ${runtime}` : '这个构建没有 iPhone 应用更新'),
|
|
345
|
-
appRuntimeChanged: (live, built) => `线上的 iPhone 应用更新是给运行时 ${live} 的,这个构建的${built ? `却是给 ${built} 的` : '没有(设了 WORKOFFDESK_IOS_BUNDLE_ID 吗?)'},装着 ${live} 的手机就再也收不到更新。App 的 JavaScript 以外的改动(它的 package.json 或 app 配置)会改变运行时。只有 App 的新版本发出去以后,才用 --new-app-runtime 部署`,
|
|
346
|
-
nothingToRollBackTo: '没有更早的版本可以回滚',
|
|
347
|
-
rollbackFailed: (target, current) => `${target} 没有起来;已切回 ${current}`,
|
|
348
|
-
rolledBack: (version, name) => `已回滚到 ${version}(${name})`,
|
|
349
|
-
},
|
|
350
|
-
account: {
|
|
351
|
-
usage: '用法:workoffdesk account [--json] | account use <邮箱或目录> [--move] | account move',
|
|
352
|
-
signedOut: '已退出登录',
|
|
353
|
-
notReady: '先在终端里用它打开一次 Claude Code',
|
|
354
|
-
apart: '它的对话没有和这台 Mac 的共用',
|
|
355
|
-
notSeen: '还没看到它的用量',
|
|
356
|
-
fiveHour: (p) => `5 小时 ${p}`,
|
|
357
|
-
sevenDay: (p) => `7 天 ${p}`,
|
|
358
|
-
reset: '已重置',
|
|
359
|
-
elsewhere: (n) => `${n} 个开着的会话在别的账号上。搬过来:workoffdesk account move`,
|
|
360
|
-
moving: (n) => `${n} 个会话等这轮回复完就搬过来。`,
|
|
361
|
-
noSuch: (who) => `这台 Mac 上没有 ${who} 这个 Claude 账号。`,
|
|
362
|
-
twoWithEmail: (email, folders) => `不止一个账号是 ${email},改用目录指定:${folders.join('、')}`,
|
|
363
|
-
nonePicked: '还没有选过账号。先选一个:workoffdesk account use <邮箱或目录>',
|
|
364
|
-
},
|
|
365
|
-
takeOver: {
|
|
366
|
-
openOnMac: (ttys, agent) => `这个对话正开在 Mac 的终端里${ttys.some(Boolean) ? `(${ttys.filter(Boolean).join('、')})` : ''};先在那里退出 ${agent},或者从手机上接管`,
|
|
367
|
-
wouldNotQuit: (agent) => `Mac 终端里的 ${agent} 10 秒内没有退出;没有续上`,
|
|
368
|
-
openElsewhere: '这个对话开在别的 Codex 应用里(编辑器或 Codex 桌面版);先在那里关掉',
|
|
369
|
-
goneToPhone: (id) => `workoffdesk:这个对话已转到 workoffdesk 里接着进行。要在这台电脑上接着用:workoffdesk attach ${id}`,
|
|
370
|
-
},
|
|
371
|
-
goOn: {
|
|
372
|
-
onlyClaude: '只有 Claude 已退出的 Claude 会话能原地接着',
|
|
373
|
-
terminalBusy: '这个会话在 Mac 的终端里正跑着别的程序;先在那里退出它,或者重新开一个对话',
|
|
374
|
-
},
|
|
375
|
-
folderGone: (path) => `它的文件夹已经不在了:${path}`,
|
|
213
|
+
folderGone: (path) => `its folder is gone: ${path}`,
|
|
376
214
|
};
|
|
377
215
|
export const messagesFor = (languages) => (pickLang(languages) === 'zh' ? zh : en);
|
|
378
216
|
export const m = messagesFor(systemLanguages());
|