sideby 0.4.1 → 0.4.3
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/dist/build.json +1 -1
- package/dist/src/cli/format.js +1 -1
- package/dist/src/cli/json-schemas.js +3 -0
- package/dist/src/core/tuning.d.ts +18 -0
- package/dist/src/core/tuning.js +34 -9
- package/dist/src/families/claude/tuning.d.ts +2 -2
- package/dist/src/families/claude/tuning.js +20 -4
- package/dist/src/panel/i18n.d.ts +1 -0
- package/dist/src/panel/i18n.js +2 -0
- package/dist/src/panel/page-script.js +7 -2
- package/dist/src/panel/page-styles.js +3 -0
- package/dist/src/types.d.ts +2 -0
- package/package.json +1 -1
- package/schemas/tune.json +5 -0
- package/skills/sideby/SKILL.md +1 -1
package/dist/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
1
|
+
{"id":"20261014T033619-9eb027"}
|
package/dist/src/cli/format.js
CHANGED
|
@@ -93,7 +93,7 @@ export function tuningLines(t) {
|
|
|
93
93
|
return [t.reason === 'no-source' ? 'no public source for this host' : (t.detail ?? t.reason)];
|
|
94
94
|
return t.models.map((m) => {
|
|
95
95
|
const now = m.configured === null ? 'Host default' : `${compactNumber(m.configured)}`;
|
|
96
|
-
const head = `${m.model} ${m.calls} calls now ${now} ${m.compactions.total} compactions`;
|
|
96
|
+
const head = `${m.model} ${m.calls} calls now ${now} ${m.compactions.total} compactions${m.compactions.idle ? ` (${m.compactions.idle} after idle)` : ''}`;
|
|
97
97
|
if (m.suggestion) {
|
|
98
98
|
const saved = m.suggestion.savedPercent === null
|
|
99
99
|
? ''
|
|
@@ -115,6 +115,9 @@ const TuningModel = Type.Object({
|
|
|
115
115
|
}),
|
|
116
116
|
auto: Type.Number(),
|
|
117
117
|
manual: Type.Number(),
|
|
118
|
+
idle: Type.Number({
|
|
119
|
+
description: 'Automatic compactions Claude Code did after the conversation sat idle; the size setting does not control them.',
|
|
120
|
+
}),
|
|
118
121
|
preTokensP50: Type.Union([Type.Number(), Type.Null()]),
|
|
119
122
|
preTokensMax: Type.Union([Type.Number(), Type.Null()]),
|
|
120
123
|
}),
|
|
@@ -13,6 +13,20 @@ export declare const WEIGHT: {
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const MIN_COMPACT_WINDOW = 100000;
|
|
15
15
|
export declare const MAX_COMPACT_WINDOW = 1000000;
|
|
16
|
+
/**
|
|
17
|
+
* Claude Code compacts a conversation on its own when it has sat idle for about 90% of the one-hour prompt cache
|
|
18
|
+
* and holds at least 200K tokens (read from Claude Code 2.1.296 and matched against real logs: 30 of 31 automatic
|
|
19
|
+
* compactions came after 54-55 minutes of silence and none was smaller than 202K). `autoCompactWindow` does not
|
|
20
|
+
* control it. A compaction after this much silence is recorded as `idle`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const IDLE_AFTER_MS: number;
|
|
23
|
+
export declare const IDLE_MIN_CTX = 200000;
|
|
24
|
+
/**
|
|
25
|
+
* How long a silence must be to call a logged compaction `idle`. Deliberately a little below `IDLE_AFTER_MS`, since the log
|
|
26
|
+
* only has the timestamp of the last message. Replaying with 45, 50 or 54 minutes gave the same simulated counts on real
|
|
27
|
+
* logs, so the difference does not matter; why the replay finds fewer idle compactions than were logged is not this.
|
|
28
|
+
*/
|
|
29
|
+
export declare const IDLE_OBSERVED_MS: number;
|
|
16
30
|
/** A model with fewer turns than this in the window gets no suggestion. */
|
|
17
31
|
export declare const MIN_CALLS = 200;
|
|
18
32
|
/** Candidate thresholds as a share of the model's context window. */
|
|
@@ -30,6 +44,8 @@ export interface Turn {
|
|
|
30
44
|
export interface Compaction {
|
|
31
45
|
ts: number;
|
|
32
46
|
trigger: 'auto' | 'manual' | 'unknown';
|
|
47
|
+
/** Claude Code did it by itself after the conversation sat idle (not because a size was reached). */
|
|
48
|
+
idle?: boolean;
|
|
33
49
|
pre: number;
|
|
34
50
|
post: number;
|
|
35
51
|
}
|
|
@@ -42,6 +58,8 @@ export interface ModelRecords {
|
|
|
42
58
|
/** Where the Host compacts when nothing is set (documented), or null when that is not known. */
|
|
43
59
|
defaultThreshold?: number | null;
|
|
44
60
|
}
|
|
61
|
+
/** Weighted cost of the sessions at a threshold. Exported for the tests of the engine. */
|
|
62
|
+
export declare function replay(sessions: Turn[][], threshold: number, post: number, idle: boolean): number;
|
|
45
63
|
export interface SummarizeOptions {
|
|
46
64
|
/** The Account's configured threshold in tokens; null is the Host default. */
|
|
47
65
|
configured: number | null;
|
package/dist/src/core/tuning.js
CHANGED
|
@@ -6,6 +6,20 @@ export const WEIGHT = { input: 1, cacheRead: 0.1, write5m: 1.25, write1h: 2, out
|
|
|
6
6
|
*/
|
|
7
7
|
export const MIN_COMPACT_WINDOW = 100_000;
|
|
8
8
|
export const MAX_COMPACT_WINDOW = 1_000_000;
|
|
9
|
+
/**
|
|
10
|
+
* Claude Code compacts a conversation on its own when it has sat idle for about 90% of the one-hour prompt cache
|
|
11
|
+
* and holds at least 200K tokens (read from Claude Code 2.1.296 and matched against real logs: 30 of 31 automatic
|
|
12
|
+
* compactions came after 54-55 minutes of silence and none was smaller than 202K). `autoCompactWindow` does not
|
|
13
|
+
* control it. A compaction after this much silence is recorded as `idle`.
|
|
14
|
+
*/
|
|
15
|
+
export const IDLE_AFTER_MS = 54 * 60_000;
|
|
16
|
+
export const IDLE_MIN_CTX = 200_000;
|
|
17
|
+
/**
|
|
18
|
+
* How long a silence must be to call a logged compaction `idle`. Deliberately a little below `IDLE_AFTER_MS`, since the log
|
|
19
|
+
* only has the timestamp of the last message. Replaying with 45, 50 or 54 minutes gave the same simulated counts on real
|
|
20
|
+
* logs, so the difference does not matter; why the replay finds fewer idle compactions than were logged is not this.
|
|
21
|
+
*/
|
|
22
|
+
export const IDLE_OBSERVED_MS = 50 * 60_000;
|
|
9
23
|
/** A model with fewer turns than this in the window gets no suggestion. */
|
|
10
24
|
export const MIN_CALLS = 200;
|
|
11
25
|
/** Candidate thresholds as a share of the model's context window. */
|
|
@@ -26,7 +40,7 @@ function writeWeight(t) {
|
|
|
26
40
|
return n > 0 ? (t.write5m * WEIGHT.write5m + t.write1h * WEIGHT.write1h) / n : WEIGHT.write5m;
|
|
27
41
|
}
|
|
28
42
|
/** Weighted cost of one session's turns if the Host compacted whenever the context reached `threshold`. */
|
|
29
|
-
function replaySession(turns, threshold, post) {
|
|
43
|
+
function replaySession(turns, threshold, post, idle) {
|
|
30
44
|
const first = turns[0];
|
|
31
45
|
if (!first)
|
|
32
46
|
return 0;
|
|
@@ -47,7 +61,9 @@ function replaySession(turns, threshold, post) {
|
|
|
47
61
|
continue;
|
|
48
62
|
const d = t.ctx - prev.ctx;
|
|
49
63
|
const g = d >= 0 ? d : usual;
|
|
50
|
-
|
|
64
|
+
// Long silence with a big context: Claude Code compacts by itself, whatever the threshold is.
|
|
65
|
+
const lapsed = idle && t.ts - prev.ts >= IDLE_AFTER_MS && ctx >= IDLE_MIN_CTX;
|
|
66
|
+
if (lapsed || ctx + g >= threshold) {
|
|
51
67
|
// The whole context goes in as input, a summary of about `post` tokens comes out, and the next turn
|
|
52
68
|
// writes that summary to the cache again.
|
|
53
69
|
cost += ctx * WEIGHT.input + post * WEIGHT.output + post * WEIGHT.write5m;
|
|
@@ -61,10 +77,11 @@ function replaySession(turns, threshold, post) {
|
|
|
61
77
|
}
|
|
62
78
|
return cost;
|
|
63
79
|
}
|
|
64
|
-
|
|
80
|
+
/** Weighted cost of the sessions at a threshold. Exported for the tests of the engine. */
|
|
81
|
+
export function replay(sessions, threshold, post, idle) {
|
|
65
82
|
let total = 0;
|
|
66
83
|
for (const s of sessions)
|
|
67
|
-
total += replaySession(s, threshold, post);
|
|
84
|
+
total += replaySession(s, threshold, post, idle);
|
|
68
85
|
return total;
|
|
69
86
|
}
|
|
70
87
|
const roundTokens = (n) => Math.round(n / 1000) * 1000;
|
|
@@ -73,14 +90,21 @@ export function summarizeModel(rec, o) {
|
|
|
73
90
|
const auto = rec.compactions.filter((c) => c.trigger === 'auto');
|
|
74
91
|
const pre = rec.compactions.map((c) => c.pre).filter((n) => n > 0);
|
|
75
92
|
const posts = rec.compactions.map((c) => c.post).filter((n) => n > 0);
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
// Threshold-triggered compactions only: the idle ones say nothing about the size Claude Code was set to.
|
|
94
|
+
const autoPre = auto
|
|
95
|
+
.filter((c) => !c.idle)
|
|
96
|
+
.map((c) => c.pre)
|
|
97
|
+
.filter((n) => n > 0);
|
|
98
|
+
const idleCount = auto.filter((c) => c.idle).length;
|
|
99
|
+
// Replay idle compaction too once the records show Claude Code doing it for this Account.
|
|
100
|
+
const idleMode = idleCount > 0;
|
|
101
|
+
// What is in effect: the configured size, else where Claude Code compacts by default, else what it did.
|
|
78
102
|
// A size outside 100K-1M is not one Claude Code runs with (the documentation does not say what it does with it),
|
|
79
103
|
// so it is not taken as the baseline.
|
|
80
104
|
const usable = o.configured !== null && o.configured >= MIN_COMPACT_WINDOW && o.configured <= MAX_COMPACT_WINDOW;
|
|
81
105
|
const effective = usable && o.configured !== null
|
|
82
106
|
? Math.min(o.configured, rec.contextWindow)
|
|
83
|
-
: (
|
|
107
|
+
: (rec.defaultThreshold ?? median(autoPre));
|
|
84
108
|
const base = {
|
|
85
109
|
model: rec.model,
|
|
86
110
|
calls,
|
|
@@ -91,6 +115,7 @@ export function summarizeModel(rec, o) {
|
|
|
91
115
|
total: rec.compactions.length,
|
|
92
116
|
auto: auto.length,
|
|
93
117
|
manual: rec.compactions.filter((c) => c.trigger === 'manual').length,
|
|
118
|
+
idle: idleCount,
|
|
94
119
|
preTokensP50: pre.length ? Math.round(median(pre)) : null,
|
|
95
120
|
preTokensMax: pre.length ? Math.max(...pre) : null,
|
|
96
121
|
},
|
|
@@ -118,7 +143,7 @@ export function summarizeModel(rec, o) {
|
|
|
118
143
|
return stop('no-optimum');
|
|
119
144
|
let best = null;
|
|
120
145
|
for (const threshold of sizes) {
|
|
121
|
-
const cost = replay(rec.sessions, threshold, post);
|
|
146
|
+
const cost = replay(rec.sessions, threshold, post, idleMode);
|
|
122
147
|
// On a tie the larger threshold wins: fewer compactions for the same cost.
|
|
123
148
|
if (!best || cost < best.cost || (cost === best.cost && threshold > best.threshold))
|
|
124
149
|
best = { threshold, cost };
|
|
@@ -132,7 +157,7 @@ export function summarizeModel(rec, o) {
|
|
|
132
157
|
reason: 'suggested',
|
|
133
158
|
suggestion: { threshold: best.threshold, savedPercent: null, savedWeighted: null },
|
|
134
159
|
};
|
|
135
|
-
const current = replay(rec.sessions, effective, post);
|
|
160
|
+
const current = replay(rec.sessions, effective, post, idleMode);
|
|
136
161
|
const saved = current - best.cost;
|
|
137
162
|
const percent = current > 0 ? (saved / current) * 100 : 0;
|
|
138
163
|
if (percent < MIN_SAVING_PERCENT)
|
|
@@ -4,8 +4,8 @@ import type { Account, ReadContext, TuningResult } from '../../types.ts';
|
|
|
4
4
|
export declare const isKnownClaudeModel: (id: string) => boolean;
|
|
5
5
|
/** `[key, ts, session, model, ctx, out, write5m, write1h]` */
|
|
6
6
|
type TurnRow = [string, number, string, string, number, number, number, number];
|
|
7
|
-
/** `[ts, session, model, trigger, pre, post]` */
|
|
8
|
-
type CompactionRow = [number, string, string, string, number, number];
|
|
7
|
+
/** `[ts, session, model, trigger, pre, post, silentMs]`; `silentMs` is how long the session was quiet before it (-1 unknown). */
|
|
8
|
+
type CompactionRow = [number, string, string, string, number, number, number];
|
|
9
9
|
/** What one session log contributes: only what the replay needs, never message text. */
|
|
10
10
|
interface Summary {
|
|
11
11
|
lines: number;
|
|
@@ -7,7 +7,7 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
8
8
|
import { FileMemo } from '../../core/file-memo.js';
|
|
9
9
|
import { USAGE_DAYS } from '../../core/quota-levels.js';
|
|
10
|
-
import { getOrCreate, tuningResult, } from '../../core/tuning.js';
|
|
10
|
+
import { getOrCreate, IDLE_OBSERVED_MS, tuningResult, } from '../../core/tuning.js';
|
|
11
11
|
import { listJsonl } from './usage.js';
|
|
12
12
|
const DAY_MS = 86_400_000;
|
|
13
13
|
const SETTINGS_MAX_BYTES = 1_000_000;
|
|
@@ -27,6 +27,8 @@ async function parseFile(file, since) {
|
|
|
27
27
|
const lastModel = new Map();
|
|
28
28
|
// Compactions logged before the first assistant row of their session in this file: they take that row's model.
|
|
29
29
|
const orphans = new Map();
|
|
30
|
+
// When each session last spoke, to tell a compaction that came after a long silence from one a size triggered.
|
|
31
|
+
const lastSpoke = new Map();
|
|
30
32
|
try {
|
|
31
33
|
const rl = createInterface({ input: createReadStream(file), crlfDelay: Number.POSITIVE_INFINITY });
|
|
32
34
|
for await (const line of rl) {
|
|
@@ -50,6 +52,7 @@ async function parseFile(file, since) {
|
|
|
50
52
|
if (!Number.isFinite(ts) || ts < since || !pre || !post)
|
|
51
53
|
continue;
|
|
52
54
|
const t = m?.trigger;
|
|
55
|
+
const before = lastSpoke.get(session);
|
|
53
56
|
const entry = [
|
|
54
57
|
ts,
|
|
55
58
|
session,
|
|
@@ -57,12 +60,19 @@ async function parseFile(file, since) {
|
|
|
57
60
|
t === 'auto' || t === 'manual' ? t : 'unknown',
|
|
58
61
|
pre,
|
|
59
62
|
post,
|
|
63
|
+
before === undefined ? -1 : ts - before,
|
|
60
64
|
];
|
|
61
65
|
out.compactions.push(entry);
|
|
66
|
+
// The compaction itself is activity: a second one right after it did not follow a silence of its own.
|
|
67
|
+
lastSpoke.set(session, ts);
|
|
62
68
|
if (!entry[2])
|
|
63
69
|
orphans.set(session, [...(orphans.get(session) ?? []), entry]);
|
|
64
70
|
continue;
|
|
65
71
|
}
|
|
72
|
+
if ((row?.type === 'assistant' || row?.type === 'user') &&
|
|
73
|
+
row.isSidechain !== true &&
|
|
74
|
+
Number.isFinite(ts))
|
|
75
|
+
lastSpoke.set(session, ts);
|
|
66
76
|
if (row?.type !== 'assistant' || row.isSidechain === true)
|
|
67
77
|
continue;
|
|
68
78
|
const usage = row.message?.usage;
|
|
@@ -101,7 +111,7 @@ async function parseFile(file, since) {
|
|
|
101
111
|
return out;
|
|
102
112
|
}
|
|
103
113
|
/** Per-file summaries per Account directory; a file is parsed again only after it changed. */
|
|
104
|
-
export const claudeTuningMemo = new FileMemo({ name: 'claude-tuning', version:
|
|
114
|
+
export const claudeTuningMemo = new FileMemo({ name: 'claude-tuning', version: 3 });
|
|
105
115
|
/** The Account's configured Compaction Threshold, or null for the Host default. Reads exactly one key. */
|
|
106
116
|
export async function readAutoCompactWindow(dir) {
|
|
107
117
|
try {
|
|
@@ -163,7 +173,7 @@ export async function readClaudeTuning(account, ctx) {
|
|
|
163
173
|
const sessionModel = new Map();
|
|
164
174
|
for (const t of [...turns.values()].sort((x, y) => x[1] - y[1]))
|
|
165
175
|
sessionModel.set(t[2], t[3]);
|
|
166
|
-
const resolved = compactions.map((c) => (c[2] ? c : [c[0], c[1], sessionModel.get(c[1]) ?? '', c[3], c[4], c[5]]));
|
|
176
|
+
const resolved = compactions.map((c) => (c[2] ? c : [c[0], c[1], sessionModel.get(c[1]) ?? '', c[3], c[4], c[5], c[6]]));
|
|
167
177
|
// Group per model, then per session; a session that switched models is replayed once per model.
|
|
168
178
|
const byModel = new Map();
|
|
169
179
|
for (const [, ts, session, model, c, out, w5, w1] of turns.values()) {
|
|
@@ -185,7 +195,13 @@ export async function readClaudeTuning(account, ctx) {
|
|
|
185
195
|
const maxCtx = Math.max(0, ...list.flat().map((t) => t.ctx));
|
|
186
196
|
const comps = resolved
|
|
187
197
|
.filter((c) => c[2] === model && c[0] >= since && c[0] <= now)
|
|
188
|
-
.map((c) => ({
|
|
198
|
+
.map((c) => ({
|
|
199
|
+
ts: c[0],
|
|
200
|
+
trigger: c[3],
|
|
201
|
+
pre: c[4],
|
|
202
|
+
post: c[5],
|
|
203
|
+
idle: c[3] === 'auto' && c[6] >= IDLE_OBSERVED_MS,
|
|
204
|
+
}));
|
|
189
205
|
const native = NATIVE_1M.test(model);
|
|
190
206
|
// Elsewhere the window comes from the records: a context past 200K means the model runs with 1M.
|
|
191
207
|
const window = native
|
package/dist/src/panel/i18n.d.ts
CHANGED
package/dist/src/panel/i18n.js
CHANGED
|
@@ -255,6 +255,7 @@ const en = {
|
|
|
255
255
|
'tune.basis': 'Based on {model} on {ref}, the one used most.',
|
|
256
256
|
'tune.outOfRange': 'The size now in the file ({size}) is outside what Claude Code accepts (100K to 1M). Pick Default or a size from the menu to replace it.',
|
|
257
257
|
'tune.overrides': 'These models save a size of their own (set with /autocompact), which wins over this one: {list}',
|
|
258
|
+
'tune.idle': '{n} of its {total} compactions came after the conversation sat idle for about an hour. Claude Code does that by itself, and this size does not change it.',
|
|
258
259
|
'tune.groupNone': 'Not used enough to suggest a size yet. You can still pick one below.',
|
|
259
260
|
'tune.details': 'Details for each account',
|
|
260
261
|
'tune.detailsObserved': 'Only observed (there is no setting to change)',
|
|
@@ -636,6 +637,7 @@ const zh = {
|
|
|
636
637
|
'tune.basis': '按用得最多的 {model}({ref})来定。',
|
|
637
638
|
'tune.outOfRange': '文件里现在的大小({size})超出了 Claude Code 接受的范围(100K 到 1M)。请选「默认」或菜单里的某个大小来替换它。',
|
|
638
639
|
'tune.overrides': '这些模型自己保存了一个大小(用 /autocompact 设的),对它们来说那个优先于这里的设置:{list}',
|
|
640
|
+
'tune.idle': '它的 {total} 次压缩里有 {n} 次是对话安静了大约一小时之后发生的。这是 Claude Code 自己做的,这个大小改不了它。',
|
|
639
641
|
'tune.groupNone': '用得还不够多,暂时给不出建议值。你仍然可以在下面自己选一个。',
|
|
640
642
|
'tune.details': '各账号的详情',
|
|
641
643
|
'tune.detailsObserved': '只做观测(没有可以改的设置)',
|
|
@@ -894,7 +894,7 @@ const MAIN = String.raw `
|
|
|
894
894
|
if (!id || (!id.email && !id.org)) return null
|
|
895
895
|
const email = id.email ? emailText(id.email) : ''
|
|
896
896
|
return h('div', { class: 'ident-email', title: [email, id.org].filter(Boolean).join(' · ') },
|
|
897
|
-
dots([email ? h('
|
|
897
|
+
dots([email ? h('button', { class: 'email email-btn', type: 'button', title: t('menu.copyEmail'), 'aria-label': t('menu.copyEmail') + ' ' + email, onclick: (e) => { e.stopPropagation(); copyText(id.email) } }, email) : null, id.org ? h('span', { class: 'org' }, id.org) : null]))
|
|
898
898
|
}
|
|
899
899
|
function subLine(a) {
|
|
900
900
|
const plan = a.quota && a.quota.status === 'ok' && a.quota.plan
|
|
@@ -1347,7 +1347,11 @@ const MAIN = String.raw `
|
|
|
1347
1347
|
pick ? h('div', null, h('span', { class: 'muted small' }, t('tune.best')), h('div', { class: 'tune-num tune-best' }, tokens(pick.threshold))) : null))
|
|
1348
1348
|
if (pick && pick.savedPercent !== null) rows.push(h('p', { class: 'small' }, t('tune.saved', { n: Math.round(pick.savedPercent) })))
|
|
1349
1349
|
if (pick) rows.push(h('p', { class: 'muted small' }, t('tune.basis', { model: pick.model, ref: pick.ref })))
|
|
1350
|
-
|
|
1350
|
+
if (pick) {
|
|
1351
|
+
const seen = entries.find((e) => e.ref === pick.ref)
|
|
1352
|
+
const stats = seen && seen.tuning.status === 'ok' ? seen.tuning.models.find((m) => m.model === pick.model) : null
|
|
1353
|
+
if (stats && stats.compactions.idle > 0) rows.push(h('p', { class: 'muted small' }, t('tune.idle', { n: stats.compactions.idle, total: stats.compactions.total })))
|
|
1354
|
+
} else {
|
|
1351
1355
|
// Say why there is no suggestion: when a model is already at its cheapest, that is the news, not "not enough use".
|
|
1352
1356
|
const why = entries.flatMap((e) => (e.tuning.status === 'ok' ? e.tuning.models : [])).some((m) => m.reason === 'already-best') ? t('tune.reason.already-best') : t('tune.groupNone')
|
|
1353
1357
|
rows.push(h('p', { class: 'note' }, why))
|
|
@@ -1376,6 +1380,7 @@ const MAIN = String.raw `
|
|
|
1376
1380
|
m.suggestion ? h('div', null, h('span', { class: 'muted small' }, t('tune.best')), h('div', { class: 'small' }, tokens(m.suggestion.threshold))) : null))
|
|
1377
1381
|
if (!m.suggestion) rows.push(h('p', { class: 'muted small' }, t('tune.reason.' + m.reason, { min: minCalls })))
|
|
1378
1382
|
rows.push(h('p', { class: 'muted small' }, m.compactions.total ? tn('tune.compactions', m.compactions.total, { size: tokens(m.compactions.preTokensP50 || 0) }) : t('tune.noCompactions')))
|
|
1383
|
+
if (m.compactions.idle > 0) rows.push(h('p', { class: 'muted small' }, t('tune.idle', { n: m.compactions.idle, total: m.compactions.total })))
|
|
1379
1384
|
return h('div', { class: 'tune-detail' }, rows)
|
|
1380
1385
|
}
|
|
1381
1386
|
function tuneDetails(entries, minCalls, label) {
|
|
@@ -167,6 +167,9 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
|
|
|
167
167
|
.ident-name { display: flex; align-items: center; gap: 6px; min-width: 0; flex-wrap: wrap; row-gap: 3px; }
|
|
168
168
|
.ident-name .name { font-weight: var(--strong-weight); font-size: var(--font-size); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
|
169
169
|
.ident-email { color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
|
|
170
|
+
.email-btn { all: unset; cursor: pointer; border-radius: 4px; }
|
|
171
|
+
.email-btn:hover { color: var(--text); text-decoration: underline dotted; }
|
|
172
|
+
.email-btn:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
|
|
170
173
|
.ident-email + .ident-sub { color: var(--faint); margin-top: 0; }
|
|
171
174
|
.tools .tb-icon { width: var(--ctl-h); height: var(--ctl-h); border-color: var(--border-strong); background: var(--surface); }
|
|
172
175
|
.tools .tb-icon:hover { background: var(--hover); }
|
package/dist/src/types.d.ts
CHANGED
|
@@ -261,6 +261,8 @@ export interface TuningModel {
|
|
|
261
261
|
total: number;
|
|
262
262
|
auto: number;
|
|
263
263
|
manual: number;
|
|
264
|
+
/** Automatic compactions Claude Code did after the conversation sat idle; the size setting does not control them. */
|
|
265
|
+
idle: number;
|
|
264
266
|
preTokensP50: number | null;
|
|
265
267
|
preTokensMax: number | null;
|
|
266
268
|
};
|
package/package.json
CHANGED
package/schemas/tune.json
CHANGED
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"total",
|
|
101
101
|
"auto",
|
|
102
102
|
"manual",
|
|
103
|
+
"idle",
|
|
103
104
|
"preTokensP50",
|
|
104
105
|
"preTokensMax"
|
|
105
106
|
],
|
|
@@ -114,6 +115,10 @@
|
|
|
114
115
|
"manual": {
|
|
115
116
|
"type": "number"
|
|
116
117
|
},
|
|
118
|
+
"idle": {
|
|
119
|
+
"type": "number",
|
|
120
|
+
"description": "Automatic compactions Claude Code did after the conversation sat idle; the size setting does not control them."
|
|
121
|
+
},
|
|
117
122
|
"preTokensP50": {
|
|
118
123
|
"anyOf": [
|
|
119
124
|
{
|
package/skills/sideby/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: sideby
|
|
3
3
|
description: Use when the user runs, or wants to run, several AI coding accounts (Claude Code, Codex, Grok Build, pi) side by side. Covers installing sideby and setting up a new account ("install sideby", "set up another Claude account", "add cc008", "配置一个 cc00x", "再加一个 Codex 账号"), explaining what accounts share and what stays separate ("what do my accounts share?", "这些账号共享了什么"), showing which account a session is ("show [cc001] in the status line", "加上账号标识"), seeing accounts, quota and usage, moving on when one hits its 5-hour or weekly limit, adding a short command (alias), and repairing drifted account directories (skills, hooks, rules or settings not shared, wrong symlinks, credential file permissions). Drives the `sideby` CLI with `--json`; the agent does every step except browser sign-in, filling in secrets and starting interactive sessions, which it hands to the user as one command.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.4.
|
|
5
|
+
version: "0.4.3"
|
|
6
6
|
requires:
|
|
7
7
|
bins: [sideby, node]
|
|
8
8
|
cliHelp: sideby --help
|