sideby 0.9.3 → 0.11.0

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.
Files changed (49) hide show
  1. package/README.md +23 -16
  2. package/dist/build.json +1 -1
  3. package/dist/src/cli/commands.d.ts +9 -1
  4. package/dist/src/cli/commands.js +145 -5
  5. package/dist/src/cli/json-schemas.js +55 -1
  6. package/dist/src/cli/main.js +23 -7
  7. package/dist/src/core/app-profiles.d.ts +50 -0
  8. package/dist/src/core/app-profiles.js +206 -0
  9. package/dist/src/core/brand-mark.d.ts +29 -0
  10. package/dist/src/core/brand-mark.js +42 -0
  11. package/dist/src/core/claude-settings-file.d.ts +17 -0
  12. package/dist/src/core/claude-settings-file.js +53 -0
  13. package/dist/src/core/desktop-app.d.ts +18 -1
  14. package/dist/src/core/desktop-app.js +22 -12
  15. package/dist/src/core/handoff-chain.d.ts +16 -2
  16. package/dist/src/core/handoff-chain.js +19 -6
  17. package/dist/src/core/handoff-run.js +43 -18
  18. package/dist/src/core/handoff-select.d.ts +4 -0
  19. package/dist/src/core/handoff-select.js +3 -2
  20. package/dist/src/core/remote-control.d.ts +45 -0
  21. package/dist/src/core/remote-control.js +157 -0
  22. package/dist/src/core/tuning-apply.js +2 -49
  23. package/dist/src/desktop/icon.js +33 -33
  24. package/dist/src/families/claude/handoff.js +10 -0
  25. package/dist/src/families/desktop.d.ts +25 -0
  26. package/dist/src/families/desktop.js +8 -2
  27. package/dist/src/families/grok/bot.d.ts +8 -0
  28. package/dist/src/families/grok/bot.js +18 -0
  29. package/dist/src/handoff/hook.js +21 -4
  30. package/dist/src/handoff/messages.d.ts +4 -0
  31. package/dist/src/handoff/messages.js +12 -0
  32. package/dist/src/panel/handler.d.ts +9 -0
  33. package/dist/src/panel/handler.js +61 -2
  34. package/dist/src/panel/i18n.d.ts +46 -0
  35. package/dist/src/panel/i18n.js +92 -0
  36. package/dist/src/panel/page-script.js +205 -5
  37. package/dist/src/panel/page-styles.js +15 -0
  38. package/dist/src/panel/page.js +2 -1
  39. package/dist/src/runtime.d.ts +10 -1
  40. package/dist/src/runtime.js +17 -0
  41. package/dist/src/types.d.ts +5 -0
  42. package/package.json +2 -2
  43. package/schemas/desktop-remove.json +33 -0
  44. package/schemas/desktop.json +2 -1
  45. package/schemas/handoff-ready.json +4 -0
  46. package/schemas/list.json +47 -0
  47. package/schemas/remote-control.json +125 -0
  48. package/skills/sideby/SKILL.md +4 -1
  49. package/skills/sideby/references/handoff.md +1 -1
@@ -7,7 +7,7 @@ import { FILES, HANDOFF_ENV, readJsonFile, writeJsonFile, } from '../core/handof
7
7
  import { claudeHookReader } from '../families/claude/handoff-hook.js';
8
8
  import { codexHookReader } from '../families/codex/handoff-hook.js';
9
9
  import { grokHookReader } from '../families/grok/handoff-hook.js';
10
- import { endTurnMessage, prepareMessage, sessionStartMessage, stopMessage, thresholdMessage, } from './messages.js';
10
+ import { endTurnMessage, notifySequence, prepareMessage, sessionStartMessage, stopMessage, thresholdMessage, waitNotice, } from './messages.js';
11
11
  import { field } from './reader.js';
12
12
  export const HOOK_COMMAND = 'sideby handoff-hook';
13
13
  const READERS = {
@@ -77,10 +77,21 @@ export async function decide(opts) {
77
77
  }
78
78
  const threshold = usable?.threshold ?? 95;
79
79
  const pressure = reader.pressure ? await reader.pressure(input, env, now) : null;
80
+ const ref = env.SIDEBY_ACCOUNT ?? family;
81
+ const waiting = usable?.decision === 'wait' && usable.until ? usable.until : undefined;
82
+ // Once per run, shown to the user (not the model): sideby will wait for the reset, and how to move on now.
83
+ const waitOutput = async () => {
84
+ if (!waiting || pressure === null || pressure < threshold || state.waitNoticeSentAt)
85
+ return undefined;
86
+ await save({ ...state, waitNoticeSentAt: now.toISOString() });
87
+ return { systemMessage: waitNotice(ref, waiting, pressure) };
88
+ };
80
89
  if (event === 'StopFailure') {
81
- if (reader.limitHit?.(input, pressure, threshold))
82
- await ready({ trigger: 'limit' });
83
- return {};
90
+ if (!reader.limitHit?.(input, pressure, threshold))
91
+ return {};
92
+ await ready({ trigger: 'limit' });
93
+ // The Host keeps only `terminalSequence` of a StopFailure hook's output.
94
+ return waiting ? { output: { terminalSequence: notifySequence(waitNotice(ref, waiting)) } } : {};
84
95
  }
85
96
  if (event === 'Stop') {
86
97
  // Grok also fires Stop when the session ends; only a finished turn counts.
@@ -95,6 +106,9 @@ export async function decide(opts) {
95
106
  }
96
107
  if (!usable)
97
108
  return {};
109
+ const notice = await waitOutput();
110
+ if (notice)
111
+ return { output: notice };
98
112
  if (state.thresholdSentAt && usable.agentBrief === false) {
99
113
  await ready({ trigger: 'threshold' });
100
114
  return {};
@@ -125,6 +139,9 @@ export async function decide(opts) {
125
139
  // PostToolUse
126
140
  if (!usable || pressure === null)
127
141
  return {};
142
+ const notice = await waitOutput();
143
+ if (notice)
144
+ return { output: notice };
128
145
  if (pressure >= usable.threshold && usable.decision === 'pick' && !state.thresholdSentAt) {
129
146
  await save({ ...state, thresholdSentAt: now.toISOString() });
130
147
  const text = usable.agentBrief === false ? endTurnMessage(pressure) : thresholdMessage(usable.brief, pressure);
@@ -12,4 +12,8 @@ declare const WHY: {
12
12
  };
13
13
  /** The next session's first prompt: the Brief's path only, never its text (it would show in a process list). */
14
14
  export declare function firstPrompt(fromRef: string, brief: string, trigger: keyof typeof WHY): string;
15
+ /** For the user, not the model (ADR-0018): this session will wait for the reset, and how to move on now instead. */
16
+ export declare function waitNotice(ref: string, until: string, pressure?: number): string;
17
+ /** A desktop notification through the Host (OSC 9 and OSC 777), the only output `StopFailure` keeps. */
18
+ export declare function notifySequence(text: string): string;
15
19
  export {};
@@ -34,3 +34,15 @@ const WHY = {
34
34
  export function firstPrompt(fromRef, brief, trigger) {
35
35
  return `This task was handed over from ${fromRef} by sideby because ${WHY[trigger]}. Read ${brief} first, then continue the task it describes. If the user asks to move the task to another account (back to ${fromRef}, say), run \`sideby handoff ready --to <account>\` and end your turn.`;
36
36
  }
37
+ const clock = (iso) => new Date(iso).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
38
+ /** For the user, not the model (ADR-0018): this session will wait for the reset, and how to move on now instead. */
39
+ export function waitNotice(ref, until, pressure) {
40
+ const now = pressure === undefined ? 'hit its limit' : `is at ${Math.round(pressure)}%`;
41
+ const then = pressure === undefined ? 'this session waits' : 'when it runs out this session waits';
42
+ return `sideby: ${ref} ${now}; its quota is back at ${clock(until)}, so ${then} and goes on by itself instead of moving to another account. To move on now, type: ! sideby handoff ready`;
43
+ }
44
+ /** A desktop notification through the Host (OSC 9 and OSC 777), the only output `StopFailure` keeps. */
45
+ export function notifySequence(text) {
46
+ const safe = [...text].map((ch) => (ch < ' ' || ch === '\u007f' ? ' ' : ch)).join('');
47
+ return `\u001b]9;${safe}\u0007\u001b]777;notify;sideby;${safe}\u0007`;
48
+ }
@@ -1,6 +1,8 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { type AppProfileStatus } from '../core/app-profiles.ts';
2
3
  import { type HandoffSettings } from '../core/config.ts';
3
4
  import { type AccountModel } from '../core/model-choice.ts';
5
+ import type { RemoteControlGroup } from '../core/remote-control.ts';
4
6
  import type { AccountStatus, DoctorHistory, FamilyInfo, Runtime } from '../runtime.ts';
5
7
  import type { Account, FamilyLogo, QuotaResult, QuotaSetupPlan, UsageResult } from '../types.ts';
6
8
  import { type PanelTheme } from './theme.ts';
@@ -94,7 +96,14 @@ export interface PanelState {
94
96
  settings: HandoffSettings;
95
97
  starters: string[];
96
98
  };
99
+ /** Claude settings files grouped as `sideby remote-control` shows them (spec §3.22); absent without Claude Accounts. */
100
+ remoteControl?: RemoteControlView[];
101
+ /** Grok Bot windows (ADR-0019), data folders shown as `~/…`; absent when there are none to show. */
102
+ appProfiles?: AppProfileStatus[];
97
103
  }
98
104
  /** The `handoff` map of the Panel state, from the statuses and Quota already read for the page. */
99
105
  export declare function handoffByFamily(rt: Runtime, accounts: readonly PanelAccount[], now: Date): Record<string, string>;
106
+ export type RemoteControlView = Omit<RemoteControlGroup, 'file'> & {
107
+ file: string;
108
+ };
100
109
  export declare function createPanelHandler(opts: PanelHandlerOptions): PanelHandler;
@@ -1,6 +1,7 @@
1
1
  // HTTP handler for the local Panel. Mountable under any basePath; standalone and embedded use the same rules.
2
2
  import { randomBytes, timingSafeEqual } from 'node:crypto';
3
3
  import { aliasesByAccount } from '../core/accounts.js';
4
+ import { listAppProfiles, openAppProfile, removeAppProfile, } from '../core/app-profiles.js';
4
5
  import { aliasArgs, aliasRefs } from '../core/config.js';
5
6
  import { openDesktopApp, withDesktopApps } from '../core/desktop-app.js';
6
7
  import { UserError } from '../core/errors.js';
@@ -197,18 +198,25 @@ async function accountState(rt, account, families) {
197
198
  ...(errors.length ? { error: errors.join('; ') } : {}),
198
199
  };
199
200
  }
201
+ function rcView(groups, home) {
202
+ return groups.map((g) => ({ ...g, file: tildify(g.file, home) }));
203
+ }
200
204
  async function buildState(rt, opts) {
201
205
  const ctx = rt.readContext();
202
- const [families, accounts, history, quotaSetups] = await Promise.all([
206
+ const [families, accounts, history, quotaSetups, remoteControl] = await Promise.all([
203
207
  rt.familyInfo(),
204
208
  rt.accounts(),
205
209
  rt.doctorHistory(),
206
210
  rt.quotaSetups(),
211
+ rt.remoteControl(),
207
212
  ]);
208
213
  const health = healthByAccount(history);
209
214
  const aliases = aliasesByAccount(aliasRefs(rt.config.aliases), accounts);
210
215
  // Desktop Apps (spec §3.20): one process listing for every Account, run alongside the per-Account reads.
211
- const states = await withDesktopApps(rt, accounts, Promise.all(accounts.map((a) => accountState(rt, a, families))));
216
+ const [states, appProfiles] = await Promise.all([
217
+ withDesktopApps(rt, accounts, Promise.all(accounts.map((a) => accountState(rt, a, families)))),
218
+ listAppProfiles(rt),
219
+ ]);
212
220
  const list = states.map((st) => {
213
221
  const h = health.get(st.ref);
214
222
  const names = aliases.get(st.ref);
@@ -239,6 +247,12 @@ async function buildState(rt, opts) {
239
247
  ...(history ? { history } : {}),
240
248
  ...(rt.config.aliases ? { configAliases: aliasRefs(rt.config.aliases) } : {}),
241
249
  ...(Object.keys(handoff).length ? { handoff } : {}),
250
+ ...(remoteControl.length ? { remoteControl: rcView(remoteControl, ctx.home) } : {}),
251
+ ...(appProfiles.length
252
+ ? {
253
+ appProfiles: appProfiles.map((p) => p.appData ? { ...p, appData: tildify(p.appData, rt.paths.home) } : p),
254
+ }
255
+ : {}),
242
256
  autoHandoff: {
243
257
  settings: rt.handoffSettings(),
244
258
  starters: [...rt.families.values()]
@@ -400,6 +414,24 @@ export function createPanelHandler(opts) {
400
414
  return { ref: found.ref, undone: r.ok, message: r.message };
401
415
  },
402
416
  },
417
+ // Remote Control off by default (spec §3.22, ADR-0017): writes `remoteControlAtStartup: false` into each Claude
418
+ // settings file that turns it on or leaves it to the default; the page shows the files first, from the state.
419
+ '/api/remote-control/off': {
420
+ async run() {
421
+ const rt = await getRuntime();
422
+ const r = await rt.remoteControlOff();
423
+ const home = rt.paths.home;
424
+ return { ...r, results: r.results.map((x) => ({ ...x, file: tildify(x.file, home) })) };
425
+ },
426
+ },
427
+ '/api/remote-control/undo': {
428
+ async run() {
429
+ const rt = await getRuntime();
430
+ const r = await rt.remoteControlUndo();
431
+ const home = rt.paths.home;
432
+ return { ...r, results: r.results.map((x) => ({ ...x, file: tildify(x.file, home) })) };
433
+ },
434
+ },
403
435
  // Auto Handoff (spec §3.17): readiness reads only; configure shows the config change unless `confirm`.
404
436
  '/api/handoff/status': {
405
437
  writes: () => false,
@@ -456,6 +488,33 @@ export function createPanelHandler(opts) {
456
488
  };
457
489
  },
458
490
  },
491
+ // Opens a Grok Bot window, creating it when the name is new, or brings it to the front (ADR-0019).
492
+ '/api/app-profile/open': {
493
+ async run(body) {
494
+ const ref = optString(body, 'ref');
495
+ if (!ref)
496
+ throw new HttpError(400, '"ref" is required');
497
+ const rt = await getRuntime();
498
+ const r = await openAppProfile(rt, ref);
499
+ const home = rt.paths.home;
500
+ return {
501
+ ...r,
502
+ appPath: tildify(r.appPath, home),
503
+ ...(r.appData ? { appData: tildify(r.appData, home) } : {}),
504
+ };
505
+ },
506
+ },
507
+ // Removes a Grok Bot window's data folder (ADR-0019). The page asks first, so this call is the confirmation.
508
+ '/api/app-profile/remove': {
509
+ async run(body) {
510
+ const ref = optString(body, 'ref');
511
+ if (!ref)
512
+ throw new HttpError(400, '"ref" is required');
513
+ const rt = await getRuntime();
514
+ const r = await removeAppProfile(rt, ref, { yes: true });
515
+ return { ...r, appData: tildify(r.appData, rt.paths.home) };
516
+ },
517
+ },
459
518
  // The Auto Handoff page (spec §3.17): where work would go next and the last hand-overs. Reads only.
460
519
  '/api/handoff/overview': {
461
520
  writes: () => false,
@@ -247,6 +247,36 @@ declare const en: {
247
247
  'desk.started': string;
248
248
  'desk.focused': string;
249
249
  'desk.failed': string;
250
+ 'app.apart': string;
251
+ 'app.apartTip': string;
252
+ 'app.mainName': string;
253
+ 'app.mainData': string;
254
+ 'app.isOpen': string;
255
+ 'app.signedIn': string;
256
+ 'app.signedOut': string;
257
+ 'app.signedInHint': string;
258
+ 'app.openBtn': string;
259
+ 'app.show': string;
260
+ 'app.openHint': string;
261
+ 'app.showHint': string;
262
+ 'app.started': string;
263
+ 'app.failed': string;
264
+ 'app.remove': string;
265
+ 'app.removeHint': string;
266
+ 'app.removeRunning': string;
267
+ 'app.removeAsk': string;
268
+ 'app.removeYes': string;
269
+ 'app.removing': string;
270
+ 'app.removed': string;
271
+ 'app.removeFailed': string;
272
+ 'app.another': string;
273
+ 'app.anotherHint': string;
274
+ 'app.nameLabel': string;
275
+ 'app.create': string;
276
+ 'app.newHint': string;
277
+ 'app.exists': string;
278
+ 'app.nameEmpty': string;
279
+ 'app.nameBad': string;
250
280
  'md.title': string;
251
281
  'md.intro': string;
252
282
  'md.model': string;
@@ -457,6 +487,22 @@ declare const en: {
457
487
  'empty.install': string;
458
488
  'empty.noFamilies': string;
459
489
  'empty.create': string;
490
+ 'rc.title': string;
491
+ 'rc.body.one': string;
492
+ 'rc.body.other': string;
493
+ 'rc.turnOff': string;
494
+ 'rc.keep': string;
495
+ 'rc.hidden': string;
496
+ 'rc.readOnly': string;
497
+ 'rc.confirmTitle': string;
498
+ 'rc.change': string;
499
+ 'rc.notSet': string;
500
+ 'rc.usedBy': string;
501
+ 'rc.confirmNote': string;
502
+ 'rc.confirm': string;
503
+ 'rc.done': string;
504
+ 'rc.failed': string;
505
+ 'rc.undone': string;
460
506
  'banner.readOnly': string;
461
507
  'banner.plugins.one': string;
462
508
  'banner.plugins.other': string;
@@ -249,6 +249,36 @@ const en = {
249
249
  'desk.started': 'Opened {app} as {ref}.',
250
250
  'desk.focused': '{app} for {ref} is in front.',
251
251
  'desk.failed': 'Could not open the desktop app for {ref}: {msg}',
252
+ 'app.apart': 'Signs in on its own',
253
+ 'app.apartTip': '{app} signs in with a Cursor account, not with any account above. Each window here keeps its own sign-in, so several can be open at once.',
254
+ 'app.mainName': 'main',
255
+ 'app.mainData': 'the app’s own data',
256
+ 'app.isOpen': 'Open',
257
+ 'app.signedIn': 'Signed in',
258
+ 'app.signedOut': 'Not signed in',
259
+ 'app.signedInHint': 'What the app last reported. Which account is signed in is not shown: sideby does not read sign-in files.',
260
+ 'app.openBtn': 'Open',
261
+ 'app.show': 'Switch to it',
262
+ 'app.openHint': 'Open {app} as {ref}, in its own window next to the others',
263
+ 'app.showHint': '{app} is open as {ref}; click to bring it to the front',
264
+ 'app.started': 'Opened {app} as {ref}.',
265
+ 'app.failed': 'Could not open {ref}: {msg}',
266
+ 'app.remove': 'Remove',
267
+ 'app.removeHint': 'Remove this {app} window: signs it out and deletes its local data on this Mac',
268
+ 'app.removeRunning': 'Quit this {app} window first, then remove it',
269
+ 'app.removeAsk': 'Remove {ref}? This signs that window out and deletes its local data on this Mac.',
270
+ 'app.removeYes': 'Remove',
271
+ 'app.removing': 'Removing…',
272
+ 'app.removed': 'Removed {ref}.',
273
+ 'app.removeFailed': 'Could not remove {ref}: {msg}',
274
+ 'app.another': 'Open another {app}',
275
+ 'app.anotherHint': 'A new window with its own sign-in.',
276
+ 'app.nameLabel': 'Name for the new window',
277
+ 'app.create': 'Create and open',
278
+ 'app.newHint': 'Sign in inside the new window with the account you want for it. Close the other {app} windows while you sign in.',
279
+ 'app.exists': '{ref} already exists; this opens it.',
280
+ 'app.nameEmpty': 'Enter a name.',
281
+ 'app.nameBad': 'Use lowercase letters, digits and dashes, starting with a letter or digit.',
252
282
  'md.title': 'Model and effort',
253
283
  'md.intro': 'Applies to new sessions of {ref} that you start with sideby (a short command or sideby run). It is saved in sideby’s config; {host}’s own settings stay as they are.',
254
284
  'md.model': 'Model',
@@ -459,6 +489,22 @@ const en = {
459
489
  'empty.install': 'Install',
460
490
  'empty.noFamilies': 'No tools are registered. Check sideby plugins.',
461
491
  'empty.create': 'Create your first account',
492
+ 'rc.title': 'Remote Control starts by itself.',
493
+ 'rc.body.one': 'Sessions of {refs} connect to claude.ai and the Claude app, where every device signed in to that account can read them.',
494
+ 'rc.body.other': 'Sessions of {n} accounts ({refs}) connect to claude.ai and the Claude app, where every device signed in to that account can read them.',
495
+ 'rc.turnOff': 'Turn all off',
496
+ 'rc.keep': 'Keep it on',
497
+ 'rc.hidden': 'Hidden in this browser. sideby remote-control off turns it off later.',
498
+ 'rc.readOnly': 'Run sideby remote-control off to turn it off.',
499
+ 'rc.confirmTitle': 'Turn off Remote Control at startup?',
500
+ 'rc.change': 'remoteControlAtStartup {from} → false',
501
+ 'rc.notSet': 'not set',
502
+ 'rc.usedBy': 'used by {refs}',
503
+ 'rc.confirmNote': 'Running sessions stay connected until you restart them. You can still start it in a session with /remote-control.',
504
+ 'rc.confirm': 'Turn off',
505
+ 'rc.done': 'Remote Control is off for new sessions. Restart sessions that are running.',
506
+ 'rc.failed': 'Could not turn it off: {msg}',
507
+ 'rc.undone': 'Undone. New sessions go back to the previous setting.',
462
508
  'banner.readOnly': 'Read-only view. Use the sideby CLI to fix or create accounts.',
463
509
  'banner.plugins.one': '{n} plugin problem.',
464
510
  'banner.plugins.other': '{n} plugin problems.',
@@ -712,6 +758,36 @@ const zh = {
712
758
  'desk.started': '已以 {ref} 打开 {app}。',
713
759
  'desk.focused': '{ref} 的 {app} 已切到前台。',
714
760
  'desk.failed': '没能打开 {ref} 的桌面应用:{msg}',
761
+ 'app.apart': '单独登录',
762
+ 'app.apartTip': '{app} 用 Cursor 账号登录,和上面的账号都无关。这里每个窗口各自保存登录,可以同时开几个。',
763
+ 'app.mainName': 'main',
764
+ 'app.mainData': '应用自己的数据',
765
+ 'app.isOpen': '已打开',
766
+ 'app.signedIn': '已登录',
767
+ 'app.signedOut': '未登录',
768
+ 'app.signedInHint': '应用最后一次报告的状态。不显示登录的是哪个账号:sideby 不读登录文件。',
769
+ 'app.openBtn': '打开',
770
+ 'app.show': '切到窗口',
771
+ 'app.openHint': '以 {ref} 打开 {app},和其他窗口同时开着',
772
+ 'app.showHint': '{app} 已经以 {ref} 打开,点一下切到前台',
773
+ 'app.started': '已以 {ref} 打开 {app}。',
774
+ 'app.failed': '没能打开 {ref}:{msg}',
775
+ 'app.remove': '删除',
776
+ 'app.removeHint': '删除这个 {app} 窗口:退出它的登录,删掉它在这台 Mac 上的数据',
777
+ 'app.removeRunning': '先退出这个 {app} 窗口,再删除',
778
+ 'app.removeAsk': '删除 {ref}?会退出这个窗口的登录,删掉它在这台 Mac 上的数据。',
779
+ 'app.removeYes': '删除',
780
+ 'app.removing': '正在删除…',
781
+ 'app.removed': '已删除 {ref}。',
782
+ 'app.removeFailed': '没能删除 {ref}:{msg}',
783
+ 'app.another': '再开一个 {app}',
784
+ 'app.anotherHint': '新窗口单独登录。',
785
+ 'app.nameLabel': '新窗口的名字',
786
+ 'app.create': '新建并打开',
787
+ 'app.newHint': '在新窗口里用你想给它的账号登录。登录时先关掉其他 {app} 窗口。',
788
+ 'app.exists': '{ref} 已经有了,点了会直接打开它。',
789
+ 'app.nameEmpty': '请填一个名字。',
790
+ 'app.nameBad': '只能用小写字母、数字和横线,以字母或数字开头。',
715
791
  'md.title': '模型和 effort',
716
792
  'md.intro': '只影响之后用 sideby 启动的 {ref} 新会话(短命令或 sideby run)。保存在 sideby 的配置里,不改 {host} 自己的设置。',
717
793
  'md.model': '模型',
@@ -922,6 +998,22 @@ const zh = {
922
998
  'empty.install': '安装',
923
999
  'empty.noFamilies': '没有注册任何工具,请检查 sideby plugins。',
924
1000
  'empty.create': '创建第一个账号',
1001
+ 'rc.title': 'Remote Control 会自动打开。',
1002
+ 'rc.body.one': '{refs} 的会话会连到 claude.ai 和 Claude 手机 App,登录这个账号的设备都能看到对话。',
1003
+ 'rc.body.other': '{n} 个账号({refs})的会话会连到 claude.ai 和 Claude 手机 App,登录这个账号的设备都能看到对话。',
1004
+ 'rc.turnOff': '全部关闭',
1005
+ 'rc.keep': '保持开启',
1006
+ 'rc.hidden': '本浏览器不再提示。以后可以运行 sideby remote-control off 关闭。',
1007
+ 'rc.readOnly': '运行 sideby remote-control off 关闭。',
1008
+ 'rc.confirmTitle': '关闭 Remote Control 自动打开?',
1009
+ 'rc.change': 'remoteControlAtStartup {from} → false',
1010
+ 'rc.notSet': '未设置',
1011
+ 'rc.usedBy': '用于 {refs}',
1012
+ 'rc.confirmNote': '已经开着的会话要重开才断开。需要时仍可在会话里运行 /remote-control 手动打开。',
1013
+ 'rc.confirm': '确认关闭',
1014
+ 'rc.done': '新会话不会再自动打开 Remote Control。开着的会话要重开。',
1015
+ 'rc.failed': '没能关闭:{msg}',
1016
+ 'rc.undone': '已撤销,新会话恢复原来的设置。',
925
1017
  'banner.readOnly': '只读视图。修复或创建账号请用 sideby 命令行。',
926
1018
  'banner.plugins.one': '{n} 个插件问题。',
927
1019
  'banner.plugins.other': '{n} 个插件问题。',