sideby 0.5.0 → 0.7.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.
- package/README.md +5 -3
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +7 -0
- package/dist/src/cli/commands.js +96 -2
- package/dist/src/cli/json-schemas.js +52 -0
- package/dist/src/cli/main.js +31 -2
- package/dist/src/core/config.d.ts +14 -0
- package/dist/src/core/config.js +9 -0
- package/dist/src/core/desktop-app.d.ts +96 -0
- package/dist/src/core/desktop-app.js +371 -0
- package/dist/src/core/doctor.d.ts +6 -1
- package/dist/src/core/doctor.js +19 -0
- package/dist/src/core/model-choice.d.ts +41 -0
- package/dist/src/core/model-choice.js +128 -0
- package/dist/src/core/tuning-apply.js +1 -1
- package/dist/src/families/claude/desktop.d.ts +6 -0
- package/dist/src/families/claude/desktop.js +28 -0
- package/dist/src/families/claude/index.js +2 -0
- package/dist/src/families/claude/model.d.ts +2 -0
- package/dist/src/families/claude/model.js +42 -0
- package/dist/src/families/claude/usage.d.ts +2 -0
- package/dist/src/families/claude/usage.js +13 -1
- package/dist/src/families/codex/desktop.d.ts +7 -0
- package/dist/src/families/codex/desktop.js +25 -0
- package/dist/src/families/codex/index.js +2 -0
- package/dist/src/families/codex/model.d.ts +2 -0
- package/dist/src/families/codex/model.js +59 -0
- package/dist/src/families/codex/rollout.d.ts +5 -0
- package/dist/src/families/codex/rollout.js +45 -3
- package/dist/src/families/desktop.d.ts +27 -0
- package/dist/src/families/desktop.js +9 -0
- package/dist/src/families/shared/model-args.d.ts +7 -0
- package/dist/src/families/shared/model-args.js +28 -0
- package/dist/src/panel/handler.d.ts +3 -0
- package/dist/src/panel/handler.js +63 -5
- package/dist/src/panel/i18n.d.ts +27 -0
- package/dist/src/panel/i18n.js +54 -0
- package/dist/src/panel/page-logic.js +2 -2
- package/dist/src/panel/page-script.js +143 -2
- package/dist/src/panel/page-styles.js +5 -1
- package/dist/src/runtime.d.ts +16 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/types.d.ts +29 -0
- package/package.json +3 -2
- package/schemas/desktop.json +80 -0
- package/schemas/list.json +20 -0
- package/schemas/model.json +169 -0
- package/schemas/quota.json +6 -0
- package/skills/sideby/SKILL.md +5 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The model and effort a Claude Code Account starts with (spec §3.19, ADR-0014). Reads only `model`,
|
|
2
|
+
// `effortLevel` and `modelSettings.<model>.effortLevel` of the Account's `settings.json`.
|
|
3
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { takeFlag } from '../shared/model-args.js';
|
|
6
|
+
const MAX_BYTES = 1_000_000;
|
|
7
|
+
const str = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
8
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
9
|
+
async function configured(account, model, lastModel) {
|
|
10
|
+
try {
|
|
11
|
+
const file = join(account.dir, 'settings.json');
|
|
12
|
+
const st = await stat(file);
|
|
13
|
+
if (!st.isFile() || st.size > MAX_BYTES)
|
|
14
|
+
return {};
|
|
15
|
+
const s = JSON.parse(await readFile(file, 'utf8'));
|
|
16
|
+
if (!isObj(s))
|
|
17
|
+
return {};
|
|
18
|
+
const m = str(s.model);
|
|
19
|
+
const ms = isObj(s.modelSettings) ? s.modelSettings : {};
|
|
20
|
+
const per = (name) => name && isObj(ms[name]) ? str(ms[name].effortLevel) : undefined;
|
|
21
|
+
// The level saved for the model the session will run: the one passed in, else the file's own, else the last one used.
|
|
22
|
+
const effort = per(model ?? m ?? lastModel) ?? str(s.effortLevel);
|
|
23
|
+
return { ...(m ? { model: m } : {}), ...(effort ? { effort } : {}) };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const claudeModelChoice = {
|
|
30
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
31
|
+
models: ['fable', 'opus', 'sonnet', 'haiku'],
|
|
32
|
+
configured,
|
|
33
|
+
toArgs: (c) => [...(c.model ? ['--model', c.model] : []), ...(c.effort ? ['--effort', c.effort] : [])],
|
|
34
|
+
fromArgs(args) {
|
|
35
|
+
const m = takeFlag(args, ['--model']);
|
|
36
|
+
const e = takeFlag(m.rest, ['--effort']);
|
|
37
|
+
return {
|
|
38
|
+
choice: { ...(m.value ? { model: m.value } : {}), ...(e.value ? { effort: e.value } : {}) },
|
|
39
|
+
rest: e.rest,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -7,6 +7,8 @@ interface Entry {
|
|
|
7
7
|
output: number;
|
|
8
8
|
cacheRead: number;
|
|
9
9
|
cacheWrite: number;
|
|
10
|
+
model?: string;
|
|
11
|
+
effort?: string;
|
|
10
12
|
}
|
|
11
13
|
export declare function listJsonl(dir: string, out?: string[]): Promise<string[]>;
|
|
12
14
|
/** What one session log holds: its usage rows in file order, keyed for the "last line wins" dedupe. */
|
|
@@ -64,6 +64,13 @@ async function parseFile(file) {
|
|
|
64
64
|
output: count(usage.output_tokens),
|
|
65
65
|
cacheRead: count(usage.cache_read_input_tokens),
|
|
66
66
|
cacheWrite: count(usage.cache_creation_input_tokens),
|
|
67
|
+
// Claude Code logs a placeholder model for some local rows; it is not a model anyone chose.
|
|
68
|
+
...(typeof row.message?.model === 'string' &&
|
|
69
|
+
row.message.model &&
|
|
70
|
+
row.message.model !== '<synthetic>'
|
|
71
|
+
? { model: row.message.model }
|
|
72
|
+
: {}),
|
|
73
|
+
...(typeof row.effort === 'string' && row.effort ? { effort: row.effort } : {}),
|
|
67
74
|
},
|
|
68
75
|
]);
|
|
69
76
|
}
|
|
@@ -75,7 +82,7 @@ async function parseFile(file) {
|
|
|
75
82
|
return out;
|
|
76
83
|
}
|
|
77
84
|
/** Parsed session logs per Account directory; a file is parsed again only after it changed. */
|
|
78
|
-
export const claudeUsageMemo = new FileMemo({ name: 'claude-usage', version:
|
|
85
|
+
export const claudeUsageMemo = new FileMemo({ name: 'claude-usage', version: 2 });
|
|
79
86
|
export async function readClaudeUsage(account, ctx) {
|
|
80
87
|
const files = await listJsonl(join(account.dir, 'projects'));
|
|
81
88
|
if (!files.length)
|
|
@@ -113,6 +120,7 @@ export async function readClaudeUsage(account, ctx) {
|
|
|
113
120
|
const sessions = new Set();
|
|
114
121
|
const days = dailyBuckets(ctx.now);
|
|
115
122
|
let last = Number.NEGATIVE_INFINITY;
|
|
123
|
+
let lastTurn;
|
|
116
124
|
let inputTokens = 0;
|
|
117
125
|
let outputTokens = 0;
|
|
118
126
|
let cacheReadTokens = 0;
|
|
@@ -122,6 +130,8 @@ export async function readClaudeUsage(account, ctx) {
|
|
|
122
130
|
continue;
|
|
123
131
|
if (e.timestamp > last)
|
|
124
132
|
last = e.timestamp;
|
|
133
|
+
if (e.model && (!lastTurn || e.timestamp >= lastTurn.timestamp))
|
|
134
|
+
lastTurn = e;
|
|
125
135
|
if (e.timestamp < since)
|
|
126
136
|
continue;
|
|
127
137
|
sessions.add(e.session);
|
|
@@ -142,5 +152,7 @@ export async function readClaudeUsage(account, ctx) {
|
|
|
142
152
|
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
143
153
|
daily: days.daily,
|
|
144
154
|
...(Number.isFinite(last) ? { lastActivityAt: new Date(last).toISOString() } : {}),
|
|
155
|
+
...(lastTurn?.model ? { lastModel: lastTurn.model } : {}),
|
|
156
|
+
...(lastTurn?.effort ? { lastEffort: lastTurn.effort } : {}),
|
|
145
157
|
};
|
|
146
158
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DesktopAppDef } from '../desktop.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The Codex app (`ChatGPT.app`, spec §3.20). It shares the Account's terminal sign-in through CODEX_HOME. Its own
|
|
4
|
+
* code replaces the path `--user-data-dir` sets, so CODEX_ELECTRON_USER_DATA_PATH goes along (the app's "Codex Demo"
|
|
5
|
+
* recipe); both are internal to the app and may change.
|
|
6
|
+
*/
|
|
7
|
+
export declare const codexDesktop: DesktopAppDef;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Codex app (`ChatGPT.app`, spec §3.20). It shares the Account's terminal sign-in through CODEX_HOME. Its own
|
|
3
|
+
* code replaces the path `--user-data-dir` sets, so CODEX_ELECTRON_USER_DATA_PATH goes along (the app's "Codex Demo"
|
|
4
|
+
* recipe); both are internal to the app and may change.
|
|
5
|
+
*/
|
|
6
|
+
export const codexDesktop = {
|
|
7
|
+
family: 'codex',
|
|
8
|
+
title: 'Codex app',
|
|
9
|
+
bundleId: 'com.openai.codex',
|
|
10
|
+
bundleNames: ['ChatGPT.app', 'Codex.app'],
|
|
11
|
+
installUrl: 'https://openai.com/codex/',
|
|
12
|
+
openArgs: (app, dir, appData) => [
|
|
13
|
+
'-n',
|
|
14
|
+
'--env',
|
|
15
|
+
`CODEX_HOME=${dir}`,
|
|
16
|
+
'--env',
|
|
17
|
+
`CODEX_ELECTRON_USER_DATA_PATH=${appData}`,
|
|
18
|
+
app,
|
|
19
|
+
'--args',
|
|
20
|
+
`--user-data-dir=${appData}`,
|
|
21
|
+
],
|
|
22
|
+
clearVars: ['CODEX_ELECTRON_USER_DATA_PATH'],
|
|
23
|
+
signInInApp: false,
|
|
24
|
+
sharedFiles: [],
|
|
25
|
+
};
|
|
@@ -5,6 +5,7 @@ import { BUILTIN_LOGOS } from '../logos.js';
|
|
|
5
5
|
import { nestedFileWrittenAt, resumeSubcommandSession } from '../shared/sessions.js';
|
|
6
6
|
import { topLevelTomlString } from '../shared/toml.js';
|
|
7
7
|
import { codexHandoff } from './handoff.js';
|
|
8
|
+
import { codexModelChoice } from './model.js';
|
|
8
9
|
import { readCodexQuota, readCodexUsage } from './rollout.js';
|
|
9
10
|
import { readCodexTuning } from './tuning.js';
|
|
10
11
|
const FILE_STORE_KEY = 'cli_auth_credentials_store';
|
|
@@ -60,6 +61,7 @@ export const codexFamily = {
|
|
|
60
61
|
}
|
|
61
62
|
},
|
|
62
63
|
model: readModel,
|
|
64
|
+
modelChoice: codexModelChoice,
|
|
63
65
|
identity: codexIdentity,
|
|
64
66
|
readQuota: (account) => readCodexQuota(account.dir),
|
|
65
67
|
readUsage: (account, ctx) => readCodexUsage(account.dir, ctx.now),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The model and effort a Codex Account starts with (spec §3.19, ADR-0014). Reads only the top-level `model` and
|
|
2
|
+
// `model_reasoning_effort` of the Account's `config.toml`.
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { takeFlag } from '../shared/model-args.js';
|
|
6
|
+
import { topLevelTomlString } from '../shared/toml.js';
|
|
7
|
+
const EFFORT_KEY = 'model_reasoning_effort';
|
|
8
|
+
async function configured(account) {
|
|
9
|
+
try {
|
|
10
|
+
const text = await readFile(join(account.dir, 'config.toml'), 'utf8');
|
|
11
|
+
const model = topLevelTomlString(text, 'model');
|
|
12
|
+
const effort = topLevelTomlString(text, EFFORT_KEY);
|
|
13
|
+
return { ...(model ? { model } : {}), ...(effort ? { effort } : {}) };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** `-c model_reasoning_effort=…` is one of possibly many `-c` overrides: only that one is taken out. */
|
|
20
|
+
function takeEffort(args) {
|
|
21
|
+
const rest = [];
|
|
22
|
+
let value;
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const a = args[i];
|
|
25
|
+
if (a === '--') {
|
|
26
|
+
rest.push(...args.slice(i));
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
const inline = a.startsWith('--config=') ? a.slice('--config='.length) : null;
|
|
30
|
+
const next = (a === '-c' || a === '--config') && i + 1 < args.length ? args[i + 1] : null;
|
|
31
|
+
const kv = inline ?? next;
|
|
32
|
+
if (kv?.startsWith(`${EFFORT_KEY}=`)) {
|
|
33
|
+
value = kv.slice(EFFORT_KEY.length + 1).replace(/^(["'])(.*)\1$/, '$2');
|
|
34
|
+
if (next !== null)
|
|
35
|
+
i++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
rest.push(a);
|
|
39
|
+
}
|
|
40
|
+
return value === undefined ? { rest } : { value, rest };
|
|
41
|
+
}
|
|
42
|
+
export const codexModelChoice = {
|
|
43
|
+
// Codex: "Available levels depend on the model and client"; these are the levels its docs name.
|
|
44
|
+
efforts: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
|
|
45
|
+
models: [],
|
|
46
|
+
configured,
|
|
47
|
+
toArgs: (c) => [
|
|
48
|
+
...(c.model ? ['-m', c.model] : []),
|
|
49
|
+
...(c.effort ? ['-c', `${EFFORT_KEY}="${c.effort}"`] : []),
|
|
50
|
+
],
|
|
51
|
+
fromArgs(args) {
|
|
52
|
+
const m = takeFlag(args, ['-m', '--model']);
|
|
53
|
+
const e = takeEffort(m.rest);
|
|
54
|
+
return {
|
|
55
|
+
choice: { ...(m.value ? { model: m.value } : {}), ...(e.value ? { effort: e.value } : {}) },
|
|
56
|
+
rest: e.rest,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -54,6 +54,11 @@ interface FileUsage {
|
|
|
54
54
|
turn: Totals | null;
|
|
55
55
|
}[];
|
|
56
56
|
deltas: Totals[];
|
|
57
|
+
context: {
|
|
58
|
+
at: number;
|
|
59
|
+
model?: string;
|
|
60
|
+
effort?: string;
|
|
61
|
+
} | null;
|
|
57
62
|
failed: boolean;
|
|
58
63
|
}
|
|
59
64
|
/** Parsed token totals per Account directory; a rollout is read again only after it changed. */
|
|
@@ -174,6 +174,42 @@ export function fileTokens(events) {
|
|
|
174
174
|
return null;
|
|
175
175
|
return fileDeltas(events).reduce(plus, ZERO);
|
|
176
176
|
}
|
|
177
|
+
/** The newest `turn_context` of a rollout: the model and effort its last turn ran with. */
|
|
178
|
+
async function lastContext(file, read) {
|
|
179
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Number.POSITIVE_INFINITY });
|
|
180
|
+
let best = null;
|
|
181
|
+
try {
|
|
182
|
+
for await (const line of lines) {
|
|
183
|
+
if (!line.includes('turn_context'))
|
|
184
|
+
continue;
|
|
185
|
+
let o;
|
|
186
|
+
try {
|
|
187
|
+
o = JSON.parse(line);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!isObj(o) || o.type !== 'turn_context' || !isObj(o.payload))
|
|
193
|
+
continue;
|
|
194
|
+
const at = typeof o.timestamp === 'string' ? Date.parse(o.timestamp) : Number.NaN;
|
|
195
|
+
if (!Number.isFinite(at) || (best && at < best.at))
|
|
196
|
+
continue;
|
|
197
|
+
const { model, effort } = o.payload;
|
|
198
|
+
best = {
|
|
199
|
+
at,
|
|
200
|
+
...(typeof model === 'string' && model ? { model } : {}),
|
|
201
|
+
...(typeof effort === 'string' && effort ? { effort } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
read.failed = true;
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
lines.close();
|
|
210
|
+
}
|
|
211
|
+
return best;
|
|
212
|
+
}
|
|
177
213
|
async function fileUsage(file) {
|
|
178
214
|
const read = { failed: false };
|
|
179
215
|
const times = [];
|
|
@@ -184,10 +220,11 @@ async function fileUsage(file) {
|
|
|
184
220
|
if (t)
|
|
185
221
|
events.push({ at, ...t });
|
|
186
222
|
}
|
|
187
|
-
|
|
223
|
+
const context = await lastContext(file, read);
|
|
224
|
+
return { times, events, deltas: fileDeltas(events), context, failed: read.failed };
|
|
188
225
|
}
|
|
189
226
|
/** Parsed token totals per Account directory; a rollout is read again only after it changed. */
|
|
190
|
-
export const codexUsageMemo = new FileMemo({ name: 'codex-usage', version:
|
|
227
|
+
export const codexUsageMemo = new FileMemo({ name: 'codex-usage', version: 2 });
|
|
191
228
|
/**
|
|
192
229
|
* Token totals for events in the last `days` x 24 h, from rollout files modified in that window (mtime only
|
|
193
230
|
* picks which files to read). Each event counts its fileDeltas increment, so a long session resumed today
|
|
@@ -207,13 +244,16 @@ export async function readCodexUsage(dir, now, days = USAGE_DAYS) {
|
|
|
207
244
|
let sawTokenCount = false;
|
|
208
245
|
let sawTotals = false;
|
|
209
246
|
let last = Number.NEGATIVE_INFINITY;
|
|
247
|
+
let lastContextSeen = null;
|
|
210
248
|
const daily = dailyBuckets(now, days);
|
|
211
249
|
const sum = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
212
250
|
const memo = codexUsageMemo.pass(dir);
|
|
213
251
|
for (const f of files) {
|
|
214
|
-
const { times, events, deltas } = await memo.get(f.path, f.stats, () => fileUsage(f.path), (v) => !v.failed);
|
|
252
|
+
const { times, events, deltas, context } = await memo.get(f.path, f.stats, () => fileUsage(f.path), (v) => !v.failed);
|
|
215
253
|
if (times.length)
|
|
216
254
|
sawTokenCount = true;
|
|
255
|
+
if (context?.model && context.at <= end && (!lastContextSeen || context.at >= lastContextSeen.at))
|
|
256
|
+
lastContextSeen = context;
|
|
217
257
|
for (const at of times)
|
|
218
258
|
if (at <= end && at > last)
|
|
219
259
|
last = at;
|
|
@@ -250,5 +290,7 @@ export async function readCodexUsage(dir, now, days = USAGE_DAYS) {
|
|
|
250
290
|
totalTokens: sum.inputTokens + sum.outputTokens + sum.cacheReadTokens + sum.cacheWriteTokens,
|
|
251
291
|
daily: daily.daily,
|
|
252
292
|
...(Number.isFinite(last) ? { lastActivityAt: new Date(last).toISOString() } : {}),
|
|
293
|
+
...(lastContextSeen?.model ? { lastModel: lastContextSeen.model } : {}),
|
|
294
|
+
...(lastContextSeen?.effort ? { lastEffort: lastContextSeen.effort } : {}),
|
|
253
295
|
};
|
|
254
296
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface DesktopAppDef {
|
|
2
|
+
/** The Family id. */
|
|
3
|
+
family: string;
|
|
4
|
+
/** How messages name the app, for example `Claude Desktop`. */
|
|
5
|
+
title: string;
|
|
6
|
+
/** `CFBundleIdentifier` in the app's Info.plist. */
|
|
7
|
+
bundleId: string;
|
|
8
|
+
/** Bundle names to look for first in /Applications and ~/Applications. */
|
|
9
|
+
bundleNames: readonly string[];
|
|
10
|
+
installUrl: string;
|
|
11
|
+
/**
|
|
12
|
+
* `open` arguments for a non-main Account: a new instance (`-n`) with the Account's selecting variable and
|
|
13
|
+
* App Data. `app` is the bundle path, `dir` the Account directory.
|
|
14
|
+
*/
|
|
15
|
+
openArgs(app: string, dir: string, appData: string): string[];
|
|
16
|
+
/** Inherited variables cleared before `open`, besides the Family's Hijack Variables and selecting variable. */
|
|
17
|
+
clearVars: readonly string[];
|
|
18
|
+
/** The first sign-in happens inside the app, through a URL callback another running instance may catch. */
|
|
19
|
+
signInInApp: boolean;
|
|
20
|
+
/** Files of the App Data shared from the Main Account's default App Data as links (`mainPath` relative to HOME). */
|
|
21
|
+
sharedFiles: readonly {
|
|
22
|
+
path: string;
|
|
23
|
+
mainPath: string;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
/** The Desktop App of a built-in Family id, if it has one. */
|
|
27
|
+
export declare function desktopDef(familyId: string): DesktopAppDef | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Desktop Apps of the built-in Families (spec §3.20, ADR-0015). Internal on purpose: not part of the public
|
|
2
|
+
// FamilyDef, so plugin Families cannot declare one in v1.
|
|
3
|
+
import { claudeDesktop } from './claude/desktop.js';
|
|
4
|
+
import { codexDesktop } from './codex/desktop.js';
|
|
5
|
+
const DESKTOP_APPS = new Map([claudeDesktop, codexDesktop].map((d) => [d.family, d]));
|
|
6
|
+
/** The Desktop App of a built-in Family id, if it has one. */
|
|
7
|
+
export function desktopDef(familyId) {
|
|
8
|
+
return DESKTOP_APPS.get(familyId);
|
|
9
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Takes every occurrence of the flags in `names` out of `args`; the last value wins. */
|
|
2
|
+
export declare function takeFlag(args: readonly string[], names: readonly string[]): {
|
|
3
|
+
value?: string;
|
|
4
|
+
rest: string[];
|
|
5
|
+
};
|
|
6
|
+
/** A model name sideby passes on: letters, digits and `._:/[]-` (for example `claude-opus-5-5[1m]`). */
|
|
7
|
+
export declare const MODEL_NAME: RegExp;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Reading and writing a model or effort choice as Host arguments (spec §3.19): the value of `--name v`, `--name=v`
|
|
2
|
+
// or `-x v`, wherever it is. Built-in Families use it for `modelChoice`.
|
|
3
|
+
/** Takes every occurrence of the flags in `names` out of `args`; the last value wins. */
|
|
4
|
+
export function takeFlag(args, names) {
|
|
5
|
+
const rest = [];
|
|
6
|
+
let value;
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const a = args[i];
|
|
9
|
+
// Past `--` everything is the Host's prompt text, not an option.
|
|
10
|
+
if (a === '--') {
|
|
11
|
+
rest.push(...args.slice(i));
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
const eq = names.find((n) => n.startsWith('--') && a.startsWith(`${n}=`));
|
|
15
|
+
if (eq) {
|
|
16
|
+
value = a.slice(eq.length + 1);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (names.includes(a) && i + 1 < args.length) {
|
|
20
|
+
value = args[++i];
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
rest.push(a);
|
|
24
|
+
}
|
|
25
|
+
return value === undefined ? { rest } : { value, rest };
|
|
26
|
+
}
|
|
27
|
+
/** A model name sideby passes on: letters, digits and `._:/[]-` (for example `claude-opus-5-5[1m]`). */
|
|
28
|
+
export const MODEL_NAME = /^[A-Za-z0-9._:/[\]-]{1,100}$/;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { type HandoffSettings } from '../core/config.ts';
|
|
3
|
+
import { type AccountModel } from '../core/model-choice.ts';
|
|
3
4
|
import type { AccountStatus, DoctorHistory, FamilyInfo, Runtime } from '../runtime.ts';
|
|
4
5
|
import type { Account, FamilyLogo, QuotaResult, QuotaSetupPlan, UsageResult } from '../types.ts';
|
|
5
6
|
import { type PanelTheme } from './theme.ts';
|
|
@@ -47,6 +48,8 @@ export type PanelAccount = (AccountStatus | (Account & Partial<AccountStatus>))
|
|
|
47
48
|
aliases?: string[];
|
|
48
49
|
/** Host arguments of those aliases that add any (alias → arguments); absent when none do. */
|
|
49
50
|
aliasArgs?: Record<string, string[]>;
|
|
51
|
+
/** The model and effort it starts with and where each comes from (spec §3.19); absent for Families without a choice. */
|
|
52
|
+
modelInfo?: Omit<AccountModel, 'ref' | 'family'>;
|
|
50
53
|
};
|
|
51
54
|
export type { DoctorHistory };
|
|
52
55
|
/** Counts of failing and warning Findings per checked Account, from the Doctor history. */
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
3
|
import { aliasesByAccount } from '../core/accounts.js';
|
|
4
4
|
import { aliasArgs, aliasRefs } from '../core/config.js';
|
|
5
|
+
import { openDesktopApp, withDesktopApps } from '../core/desktop-app.js';
|
|
5
6
|
import { UserError } from '../core/errors.js';
|
|
6
7
|
import { planHandoff } from '../core/handoff.js';
|
|
7
8
|
import { applyHandoffEnable, planHandoffEnable } from '../core/handoff-enable.js';
|
|
8
9
|
import { handoffOverview } from '../core/handoff-overview.js';
|
|
9
10
|
import { configureHandoff, handoffStatus } from '../core/handoff-status.js';
|
|
11
|
+
import { accountModel, setAccountModel } from '../core/model-choice.js';
|
|
10
12
|
import { tildify } from '../core/paths.js';
|
|
11
13
|
import { MIN_CALLS } from '../core/tuning.js';
|
|
12
14
|
import { buildId, packageVersion } from '../core/version.js';
|
|
@@ -170,8 +172,23 @@ async function accountState(rt, account, families) {
|
|
|
170
172
|
login: 'unknown',
|
|
171
173
|
};
|
|
172
174
|
errors.push(...(status?.problems ?? []));
|
|
175
|
+
let modelInfo;
|
|
176
|
+
if (rt.families.get(account.family)?.modelChoice) {
|
|
177
|
+
const last = usage?.status === 'ok'
|
|
178
|
+
? {
|
|
179
|
+
...(usage.lastModel ? { model: usage.lastModel } : {}),
|
|
180
|
+
...(usage.lastEffort ? { effort: usage.lastEffort } : {}),
|
|
181
|
+
}
|
|
182
|
+
: {};
|
|
183
|
+
const info = await guard(accountModel(rt, account, last));
|
|
184
|
+
if (info) {
|
|
185
|
+
const { ref: _r, family: _f, ...rest } = info;
|
|
186
|
+
modelInfo = rest;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
173
189
|
return {
|
|
174
190
|
...base,
|
|
191
|
+
...(modelInfo ? { modelInfo } : {}),
|
|
175
192
|
...(quota ? { quota } : {}),
|
|
176
193
|
...(usage ? { usage } : {}),
|
|
177
194
|
...(errors.length ? { error: errors.join('; ') } : {}),
|
|
@@ -187,10 +204,11 @@ async function buildState(rt, opts) {
|
|
|
187
204
|
]);
|
|
188
205
|
const health = healthByAccount(history);
|
|
189
206
|
const aliases = aliasesByAccount(aliasRefs(rt.config.aliases), accounts);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
207
|
+
// Desktop Apps (spec §3.20): one process listing for every Account, run alongside the per-Account reads.
|
|
208
|
+
const states = await withDesktopApps(rt, accounts, Promise.all(accounts.map((a) => accountState(rt, a, families))));
|
|
209
|
+
const list = states.map((st) => {
|
|
210
|
+
const h = health.get(st.ref);
|
|
211
|
+
const names = aliases.get(st.ref);
|
|
194
212
|
const withArgs = (names ?? [])
|
|
195
213
|
.map((n) => [n, aliasArgs(rt.config.aliases?.[n])])
|
|
196
214
|
.filter(([, args]) => args.length);
|
|
@@ -200,7 +218,7 @@ async function buildState(rt, opts) {
|
|
|
200
218
|
...(names ? { aliases: names } : {}),
|
|
201
219
|
...(withArgs.length ? { aliasArgs: Object.fromEntries(withArgs) } : {}),
|
|
202
220
|
};
|
|
203
|
-
})
|
|
221
|
+
});
|
|
204
222
|
const handoff = handoffByFamily(rt, list, ctx.now);
|
|
205
223
|
return {
|
|
206
224
|
schemaVersion: 1,
|
|
@@ -395,6 +413,46 @@ export function createPanelHandler(opts) {
|
|
|
395
413
|
return { ...r, file: tildify(r.file, rt.paths.home) };
|
|
396
414
|
},
|
|
397
415
|
},
|
|
416
|
+
// Model and effort per Account (spec §3.19, ADR-0014): without `confirm` only the config diff.
|
|
417
|
+
// `model` / `effort`: left out keeps it, null goes back to the Host's default, a string sets it.
|
|
418
|
+
'/api/account/model': {
|
|
419
|
+
writes: (body) => body.confirm === true,
|
|
420
|
+
async run(body) {
|
|
421
|
+
const account = optString(body, 'account');
|
|
422
|
+
if (!account)
|
|
423
|
+
throw new HttpError(400, '"account" is required');
|
|
424
|
+
const change = {};
|
|
425
|
+
for (const k of ['model', 'effort']) {
|
|
426
|
+
const v = body[k];
|
|
427
|
+
if (v === undefined)
|
|
428
|
+
continue;
|
|
429
|
+
if (v !== null && typeof v !== 'string')
|
|
430
|
+
throw new HttpError(400, `"${k}" must be a string, or null for the default`);
|
|
431
|
+
change[k] = v;
|
|
432
|
+
}
|
|
433
|
+
const rt = await getRuntime();
|
|
434
|
+
const found = await rt.resolve(account);
|
|
435
|
+
const r = await setAccountModel(rt, found, change, { apply: optBool(body, 'confirm') });
|
|
436
|
+
return { ref: found.ref, ...r, file: tildify(r.file, rt.paths.home) };
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
// Opens the Account's Desktop App, or brings its window to the front (spec §3.20). It creates the Account's
|
|
440
|
+
// app data folder and starts a program, so a read-only Panel refuses it (writes defaults to true).
|
|
441
|
+
'/api/account/desktop': {
|
|
442
|
+
async run(body) {
|
|
443
|
+
const account = optString(body, 'account');
|
|
444
|
+
if (!account)
|
|
445
|
+
throw new HttpError(400, '"account" is required');
|
|
446
|
+
const rt = await getRuntime();
|
|
447
|
+
const r = await openDesktopApp(rt, account);
|
|
448
|
+
const home = rt.paths.home;
|
|
449
|
+
return {
|
|
450
|
+
...r,
|
|
451
|
+
appPath: tildify(r.appPath, home),
|
|
452
|
+
...(r.appData ? { appData: tildify(r.appData, home) } : {}),
|
|
453
|
+
};
|
|
454
|
+
},
|
|
455
|
+
},
|
|
398
456
|
// The Auto Handoff page (spec §3.17): where work would go next and the last hand-overs. Reads only.
|
|
399
457
|
'/api/handoff/overview': {
|
|
400
458
|
writes: () => false,
|
package/dist/src/panel/i18n.d.ts
CHANGED
|
@@ -233,6 +233,33 @@ declare const en: {
|
|
|
233
233
|
'menu.check': string;
|
|
234
234
|
'menu.like': string;
|
|
235
235
|
'menu.tune': string;
|
|
236
|
+
'menu.model': string;
|
|
237
|
+
'desk.open': string;
|
|
238
|
+
'desk.show': string;
|
|
239
|
+
'desk.opening': string;
|
|
240
|
+
'desk.openHint': string;
|
|
241
|
+
'desk.showHint': string;
|
|
242
|
+
'desk.started': string;
|
|
243
|
+
'desk.focused': string;
|
|
244
|
+
'desk.failed': string;
|
|
245
|
+
'md.title': string;
|
|
246
|
+
'md.intro': string;
|
|
247
|
+
'md.model': string;
|
|
248
|
+
'md.effort': string;
|
|
249
|
+
'md.default': string;
|
|
250
|
+
'md.decides': string;
|
|
251
|
+
'md.from.sideby': string;
|
|
252
|
+
'md.from.host': string;
|
|
253
|
+
'md.from.last': string;
|
|
254
|
+
'md.none': string;
|
|
255
|
+
'md.click': string;
|
|
256
|
+
'md.envNote': string;
|
|
257
|
+
'md.reset': string;
|
|
258
|
+
'md.review': string;
|
|
259
|
+
'md.apply': string;
|
|
260
|
+
'md.nothing': string;
|
|
261
|
+
'md.changes': string;
|
|
262
|
+
'md.saved': string;
|
|
236
263
|
'page.handoff': string;
|
|
237
264
|
'hp.title': string;
|
|
238
265
|
'hp.advanced': string;
|
package/dist/src/panel/i18n.js
CHANGED
|
@@ -235,6 +235,33 @@ const en = {
|
|
|
235
235
|
'menu.check': 'Check this account',
|
|
236
236
|
'menu.like': 'New account like this',
|
|
237
237
|
'menu.tune': 'Context tuning',
|
|
238
|
+
'menu.model': 'Model and effort…',
|
|
239
|
+
'desk.open': 'Open desktop app',
|
|
240
|
+
'desk.show': 'Desktop app open',
|
|
241
|
+
'desk.opening': 'Opening {app}…',
|
|
242
|
+
'desk.openHint': 'Open {app} as {ref}, in its own window next to your other accounts',
|
|
243
|
+
'desk.showHint': '{app} is open as {ref}; click to bring it to the front',
|
|
244
|
+
'desk.started': 'Opened {app} as {ref}.',
|
|
245
|
+
'desk.focused': '{app} for {ref} is in front.',
|
|
246
|
+
'desk.failed': 'Could not open the desktop app for {ref}: {msg}',
|
|
247
|
+
'md.title': 'Model and effort',
|
|
248
|
+
'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.',
|
|
249
|
+
'md.model': 'Model',
|
|
250
|
+
'md.effort': 'Effort',
|
|
251
|
+
'md.default': 'Default',
|
|
252
|
+
'md.decides': '{host} decides',
|
|
253
|
+
'md.from.sideby': 'set by sideby for this account',
|
|
254
|
+
'md.from.host': 'from {host}’s settings',
|
|
255
|
+
'md.from.last': 'used in the last session',
|
|
256
|
+
'md.none': 'not known yet',
|
|
257
|
+
'md.click': 'Click to change.',
|
|
258
|
+
'md.envNote': 'ANTHROPIC_MODEL or CLAUDE_CODE_EFFORT_LEVEL in your shell, and settings from your organization, override this; sideby cannot see them.',
|
|
259
|
+
'md.reset': 'Back to default',
|
|
260
|
+
'md.review': 'Review change',
|
|
261
|
+
'md.apply': 'Apply',
|
|
262
|
+
'md.nothing': 'Nothing to change.',
|
|
263
|
+
'md.changes': 'This changes sideby’s config:',
|
|
264
|
+
'md.saved': 'Saved. New sessions of {ref} use it.',
|
|
238
265
|
'page.handoff': 'Auto Handoff',
|
|
239
266
|
'hp.title': 'Auto Handoff',
|
|
240
267
|
'hp.advanced': 'Advanced settings',
|
|
@@ -665,6 +692,33 @@ const zh = {
|
|
|
665
692
|
'menu.check': '检查这个账号',
|
|
666
693
|
'menu.like': '照此新建账号',
|
|
667
694
|
'menu.tune': '上下文调优',
|
|
695
|
+
'menu.model': '模型和 effort…',
|
|
696
|
+
'desk.open': '打开桌面应用',
|
|
697
|
+
'desk.show': '桌面应用已打开',
|
|
698
|
+
'desk.opening': '正在打开 {app}…',
|
|
699
|
+
'desk.openHint': '以 {ref} 打开 {app},和其他账号的窗口同时开着',
|
|
700
|
+
'desk.showHint': '{app} 已经以 {ref} 打开,点一下切到前台',
|
|
701
|
+
'desk.started': '已以 {ref} 打开 {app}。',
|
|
702
|
+
'desk.focused': '{ref} 的 {app} 已切到前台。',
|
|
703
|
+
'desk.failed': '没能打开 {ref} 的桌面应用:{msg}',
|
|
704
|
+
'md.title': '模型和 effort',
|
|
705
|
+
'md.intro': '只影响之后用 sideby 启动的 {ref} 新会话(短命令或 sideby run)。保存在 sideby 的配置里,不改 {host} 自己的设置。',
|
|
706
|
+
'md.model': '模型',
|
|
707
|
+
'md.effort': 'Effort',
|
|
708
|
+
'md.default': '默认',
|
|
709
|
+
'md.decides': '由 {host} 决定',
|
|
710
|
+
'md.from.sideby': 'sideby 为这个账号设置',
|
|
711
|
+
'md.from.host': '来自 {host} 的设置',
|
|
712
|
+
'md.from.last': '最近一次会话用的',
|
|
713
|
+
'md.none': '暂时不知道',
|
|
714
|
+
'md.click': '点击修改。',
|
|
715
|
+
'md.envNote': 'shell 里的 ANTHROPIC_MODEL、CLAUDE_CODE_EFFORT_LEVEL 和组织下发的设置会覆盖这里,sideby 看不到它们。',
|
|
716
|
+
'md.reset': '恢复默认',
|
|
717
|
+
'md.review': '查看改动',
|
|
718
|
+
'md.apply': '应用',
|
|
719
|
+
'md.nothing': '没有要改的。',
|
|
720
|
+
'md.changes': '将修改 sideby 的配置:',
|
|
721
|
+
'md.saved': '已保存,{ref} 的新会话会用它。',
|
|
668
722
|
'page.handoff': '自动交接',
|
|
669
723
|
'hp.title': '自动交接',
|
|
670
724
|
'hp.advanced': '高级设置',
|
|
@@ -257,8 +257,8 @@ export function suggestName(names) {
|
|
|
257
257
|
if (!taken.work)
|
|
258
258
|
return 'work';
|
|
259
259
|
for (let i = 2;; i++)
|
|
260
|
-
if (!taken[
|
|
261
|
-
return
|
|
260
|
+
if (!taken[`work${i}`])
|
|
261
|
+
return `work${i}`;
|
|
262
262
|
}
|
|
263
263
|
/**
|
|
264
264
|
* A Finding's hint as the Panel shows it: for a fixable Finding, "run `sideby doctor --fix`" becomes the
|