gsd-pi 2.17.0 → 2.19.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 +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +399 -29
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +382 -23
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/dist/resources/extensions/gsd/files.ts +123 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/metrics.ts +48 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +132 -1
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +22 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/dist/resources/extensions/remote-questions/format.ts +12 -6
- package/dist/resources/extensions/remote-questions/manager.ts +8 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +399 -29
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +382 -23
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/src/resources/extensions/gsd/files.ts +123 -1
- package/src/resources/extensions/gsd/guided-flow.ts +237 -4
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/metrics.ts +48 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +132 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +22 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/src/resources/extensions/remote-questions/format.ts +12 -6
- package/src/resources/extensions/remote-questions/manager.ts +8 -0
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe read-modify-write for models.json with file locking.
|
|
3
|
+
* Prevents concurrent writes from corrupting the config file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
import lockfile from "proper-lockfile";
|
|
9
|
+
import { getAgentDir } from "../config.js";
|
|
10
|
+
|
|
11
|
+
interface ModelDefinition {
|
|
12
|
+
id: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
api?: string;
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
reasoning?: boolean;
|
|
17
|
+
input?: ("text" | "image")[];
|
|
18
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
19
|
+
contextWindow?: number;
|
|
20
|
+
maxTokens?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ProviderConfig {
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
api?: string;
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
authHeader?: boolean;
|
|
29
|
+
models?: ModelDefinition[];
|
|
30
|
+
modelOverrides?: Record<string, Record<string, unknown>>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ModelsConfig {
|
|
34
|
+
providers: Record<string, ProviderConfig>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ModelsJsonWriter {
|
|
38
|
+
private modelsJsonPath: string;
|
|
39
|
+
|
|
40
|
+
constructor(modelsJsonPath?: string) {
|
|
41
|
+
this.modelsJsonPath = modelsJsonPath ?? join(getAgentDir(), "models.json");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Add a model to a provider. Creates the provider if it doesn't exist.
|
|
46
|
+
*/
|
|
47
|
+
addModel(provider: string, model: ModelDefinition, providerConfig?: Partial<ProviderConfig>): void {
|
|
48
|
+
this.withLock((config) => {
|
|
49
|
+
if (!config.providers[provider]) {
|
|
50
|
+
config.providers[provider] = {
|
|
51
|
+
...providerConfig,
|
|
52
|
+
models: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const providerEntry = config.providers[provider];
|
|
57
|
+
if (!providerEntry.models) {
|
|
58
|
+
providerEntry.models = [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Replace existing model with same id, or append
|
|
62
|
+
const existingIndex = providerEntry.models.findIndex((m) => m.id === model.id);
|
|
63
|
+
if (existingIndex >= 0) {
|
|
64
|
+
providerEntry.models[existingIndex] = model;
|
|
65
|
+
} else {
|
|
66
|
+
providerEntry.models.push(model);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return config;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Remove a model from a provider. Removes the provider if no models remain.
|
|
75
|
+
*/
|
|
76
|
+
removeModel(provider: string, modelId: string): void {
|
|
77
|
+
this.withLock((config) => {
|
|
78
|
+
const providerEntry = config.providers[provider];
|
|
79
|
+
if (!providerEntry?.models) return config;
|
|
80
|
+
|
|
81
|
+
providerEntry.models = providerEntry.models.filter((m) => m.id !== modelId);
|
|
82
|
+
|
|
83
|
+
// Clean up empty provider (no models and no overrides)
|
|
84
|
+
if (providerEntry.models.length === 0 && !providerEntry.modelOverrides) {
|
|
85
|
+
delete config.providers[provider];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return config;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Set or update an entire provider configuration.
|
|
94
|
+
*/
|
|
95
|
+
setProvider(provider: string, providerConfig: ProviderConfig): void {
|
|
96
|
+
this.withLock((config) => {
|
|
97
|
+
config.providers[provider] = providerConfig;
|
|
98
|
+
return config;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove a provider and all its models.
|
|
104
|
+
*/
|
|
105
|
+
removeProvider(provider: string): void {
|
|
106
|
+
this.withLock((config) => {
|
|
107
|
+
delete config.providers[provider];
|
|
108
|
+
return config;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* List all providers and their configurations.
|
|
114
|
+
*/
|
|
115
|
+
listProviders(): ModelsConfig {
|
|
116
|
+
return this.readConfig();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private readConfig(): ModelsConfig {
|
|
120
|
+
if (!existsSync(this.modelsJsonPath)) {
|
|
121
|
+
return { providers: {} };
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const content = readFileSync(this.modelsJsonPath, "utf-8");
|
|
125
|
+
return JSON.parse(content) as ModelsConfig;
|
|
126
|
+
} catch {
|
|
127
|
+
return { providers: {} };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private writeConfig(config: ModelsConfig): void {
|
|
132
|
+
const dir = dirname(this.modelsJsonPath);
|
|
133
|
+
if (!existsSync(dir)) {
|
|
134
|
+
mkdirSync(dir, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
writeFileSync(this.modelsJsonPath, JSON.stringify(config, null, 2), "utf-8");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private acquireLockWithRetry(): () => void {
|
|
140
|
+
const maxAttempts = 10;
|
|
141
|
+
const delayMs = 20;
|
|
142
|
+
let lastError: unknown;
|
|
143
|
+
|
|
144
|
+
// Ensure file exists for locking
|
|
145
|
+
const dir = dirname(this.modelsJsonPath);
|
|
146
|
+
if (!existsSync(dir)) {
|
|
147
|
+
mkdirSync(dir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
if (!existsSync(this.modelsJsonPath)) {
|
|
150
|
+
writeFileSync(this.modelsJsonPath, JSON.stringify({ providers: {} }, null, 2), "utf-8");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
154
|
+
try {
|
|
155
|
+
return lockfile.lockSync(this.modelsJsonPath, { realpath: false });
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const code =
|
|
158
|
+
typeof error === "object" && error !== null && "code" in error
|
|
159
|
+
? String((error as { code?: unknown }).code)
|
|
160
|
+
: undefined;
|
|
161
|
+
if (code !== "ELOCKED" || attempt === maxAttempts) {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
lastError = error;
|
|
165
|
+
const start = Date.now();
|
|
166
|
+
while (Date.now() - start < delayMs) {
|
|
167
|
+
// Busy-wait (same pattern as auth-storage.ts)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw (lastError as Error) ?? new Error("Failed to acquire models.json lock");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private withLock(fn: (config: ModelsConfig) => ModelsConfig): void {
|
|
176
|
+
let release: (() => void) | undefined;
|
|
177
|
+
try {
|
|
178
|
+
release = this.acquireLockWithRetry();
|
|
179
|
+
const config = this.readConfig();
|
|
180
|
+
const updated = fn(config);
|
|
181
|
+
this.writeConfig(updated);
|
|
182
|
+
} finally {
|
|
183
|
+
if (release) {
|
|
184
|
+
release();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -79,6 +79,13 @@ export interface FallbackSettings {
|
|
|
79
79
|
chains?: Record<string, FallbackChainEntry[]>; // keyed by chain name
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export interface ModelDiscoverySettings {
|
|
83
|
+
enabled?: boolean; // default: false
|
|
84
|
+
providers?: string[]; // limit discovery to specific providers
|
|
85
|
+
ttlMinutes?: number; // override default TTLs (in minutes)
|
|
86
|
+
autoRefreshOnModelSelect?: boolean; // default: false - refresh discovery when opening model selector
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
export type TransportSetting = Transport;
|
|
83
90
|
|
|
84
91
|
/**
|
|
@@ -134,6 +141,7 @@ export interface Settings {
|
|
|
134
141
|
bashInterceptor?: BashInterceptorSettings;
|
|
135
142
|
taskIsolation?: TaskIsolationSettings;
|
|
136
143
|
fallback?: FallbackSettings;
|
|
144
|
+
modelDiscovery?: ModelDiscoverySettings;
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
@@ -1076,4 +1084,17 @@ export class SettingsManager {
|
|
|
1076
1084
|
chains: this.getFallbackChains(),
|
|
1077
1085
|
};
|
|
1078
1086
|
}
|
|
1087
|
+
|
|
1088
|
+
getModelDiscoverySettings(): ModelDiscoverySettings {
|
|
1089
|
+
return this.settings.modelDiscovery ?? {};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
setModelDiscoveryEnabled(enabled: boolean): void {
|
|
1093
|
+
if (!this.globalSettings.modelDiscovery) {
|
|
1094
|
+
this.globalSettings.modelDiscovery = {};
|
|
1095
|
+
}
|
|
1096
|
+
this.globalSettings.modelDiscovery.enabled = enabled;
|
|
1097
|
+
this.markModified("modelDiscovery", "enabled");
|
|
1098
|
+
this.save();
|
|
1099
|
+
}
|
|
1079
1100
|
}
|
|
@@ -28,6 +28,7 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
|
|
28
28
|
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
29
29
|
{ name: "fork", description: "Create a new fork from a previous message" },
|
|
30
30
|
{ name: "tree", description: "Navigate session tree (switch branches)" },
|
|
31
|
+
{ name: "provider", description: "Manage provider configuration" },
|
|
31
32
|
{ name: "login", description: "Login with OAuth provider" },
|
|
32
33
|
{ name: "logout", description: "Logout from OAuth provider" },
|
|
33
34
|
{ name: "new", description: "Start a new session" },
|
|
@@ -143,7 +143,11 @@ export {
|
|
|
143
143
|
// Footer data provider (git branch + extension statuses - data not otherwise available to extensions)
|
|
144
144
|
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js";
|
|
145
145
|
export { convertToLlm } from "./core/messages.js";
|
|
146
|
+
export { ModelDiscoveryCache } from "./core/discovery-cache.js";
|
|
147
|
+
export type { DiscoveredModel, DiscoveryResult, ProviderDiscoveryAdapter } from "./core/model-discovery.js";
|
|
148
|
+
export { getDiscoverableProviders, getDiscoveryAdapter } from "./core/model-discovery.js";
|
|
146
149
|
export { ModelRegistry } from "./core/model-registry.js";
|
|
150
|
+
export { ModelsJsonWriter } from "./core/models-json-writer.js";
|
|
147
151
|
export type {
|
|
148
152
|
PackageManager,
|
|
149
153
|
PathMetadata,
|
|
@@ -307,6 +311,7 @@ export {
|
|
|
307
311
|
LoginDialogComponent,
|
|
308
312
|
ModelSelectorComponent,
|
|
309
313
|
OAuthSelectorComponent,
|
|
314
|
+
ProviderManagerComponent,
|
|
310
315
|
type RenderDiffOptions,
|
|
311
316
|
rawKeyHint,
|
|
312
317
|
renderDiff,
|
|
@@ -11,7 +11,7 @@ import { createInterface } from "readline";
|
|
|
11
11
|
import { type Args, parseArgs, printHelp } from "./cli/args.js";
|
|
12
12
|
import { selectConfig } from "./cli/config-selector.js";
|
|
13
13
|
import { processFileArguments } from "./cli/file-processor.js";
|
|
14
|
-
import { listModels } from "./cli/list-models.js";
|
|
14
|
+
import { discoverAndPrintModels, listModels } from "./cli/list-models.js";
|
|
15
15
|
import { selectSession } from "./cli/session-picker.js";
|
|
16
16
|
import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
|
|
17
17
|
import { AuthStorage } from "./core/auth-storage.js";
|
|
@@ -660,9 +660,26 @@ export async function main(args: string[]) {
|
|
|
660
660
|
process.exit(0);
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
+
if (parsed.addProvider) {
|
|
664
|
+
const { ModelsJsonWriter } = await import("./core/models-json-writer.js");
|
|
665
|
+
const writer = new ModelsJsonWriter();
|
|
666
|
+
writer.setProvider(parsed.addProvider, {
|
|
667
|
+
baseUrl: parsed.addProviderBaseUrl,
|
|
668
|
+
apiKey: parsed.apiKey,
|
|
669
|
+
});
|
|
670
|
+
console.log(`Provider "${parsed.addProvider}" added to models.json`);
|
|
671
|
+
process.exit(0);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (parsed.discoverModels !== undefined) {
|
|
675
|
+
const provider = typeof parsed.discoverModels === "string" ? parsed.discoverModels : undefined;
|
|
676
|
+
await discoverAndPrintModels(modelRegistry, provider);
|
|
677
|
+
process.exit(0);
|
|
678
|
+
}
|
|
679
|
+
|
|
663
680
|
if (parsed.listModels !== undefined) {
|
|
664
681
|
const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
|
|
665
|
-
await listModels(modelRegistry, searchPattern);
|
|
682
|
+
await listModels(modelRegistry, { searchPattern, discover: parsed.discover });
|
|
666
683
|
process.exit(0);
|
|
667
684
|
}
|
|
668
685
|
|
|
@@ -18,6 +18,7 @@ export { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./keybinding
|
|
|
18
18
|
export { LoginDialogComponent } from "./login-dialog.js";
|
|
19
19
|
export { ModelSelectorComponent } from "./model-selector.js";
|
|
20
20
|
export { OAuthSelectorComponent } from "./oauth-selector.js";
|
|
21
|
+
export { ProviderManagerComponent } from "./provider-manager.js";
|
|
21
22
|
export { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from "./scoped-models-selector.js";
|
|
22
23
|
export { SessionSelectorComponent } from "./session-selector.js";
|
|
23
24
|
export { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from "./settings-selector.js";
|
|
@@ -160,7 +160,7 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
160
160
|
|
|
161
161
|
// Load available models (built-in models still work even if models.json failed)
|
|
162
162
|
try {
|
|
163
|
-
const availableModels =
|
|
163
|
+
const availableModels = this.modelRegistry.getAvailable();
|
|
164
164
|
models = availableModels.map((model: Model<any>) => ({
|
|
165
165
|
provider: model.provider,
|
|
166
166
|
id: model.id,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI component for managing provider configurations.
|
|
3
|
+
* Shows providers with auth status, discovery support, and model counts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Container,
|
|
8
|
+
type Focusable,
|
|
9
|
+
getEditorKeybindings,
|
|
10
|
+
Spacer,
|
|
11
|
+
Text,
|
|
12
|
+
type TUI,
|
|
13
|
+
} from "@gsd/pi-tui";
|
|
14
|
+
import type { AuthStorage } from "../../../core/auth-storage.js";
|
|
15
|
+
import { getDiscoverableProviders } from "../../../core/model-discovery.js";
|
|
16
|
+
import type { ModelRegistry } from "../../../core/model-registry.js";
|
|
17
|
+
import { theme } from "../theme/theme.js";
|
|
18
|
+
import { rawKeyHint } from "./keybinding-hints.js";
|
|
19
|
+
|
|
20
|
+
interface ProviderInfo {
|
|
21
|
+
name: string;
|
|
22
|
+
hasAuth: boolean;
|
|
23
|
+
supportsDiscovery: boolean;
|
|
24
|
+
modelCount: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ProviderManagerComponent extends Container implements Focusable {
|
|
28
|
+
private _focused = false;
|
|
29
|
+
get focused(): boolean {
|
|
30
|
+
return this._focused;
|
|
31
|
+
}
|
|
32
|
+
set focused(value: boolean) {
|
|
33
|
+
this._focused = value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private providers: ProviderInfo[] = [];
|
|
37
|
+
private selectedIndex = 0;
|
|
38
|
+
private listContainer: Container;
|
|
39
|
+
private tui: TUI;
|
|
40
|
+
private authStorage: AuthStorage;
|
|
41
|
+
private modelRegistry: ModelRegistry;
|
|
42
|
+
private onDone: () => void;
|
|
43
|
+
private onDiscover: (provider: string) => void;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
tui: TUI,
|
|
47
|
+
authStorage: AuthStorage,
|
|
48
|
+
modelRegistry: ModelRegistry,
|
|
49
|
+
onDone: () => void,
|
|
50
|
+
onDiscover: (provider: string) => void,
|
|
51
|
+
) {
|
|
52
|
+
super();
|
|
53
|
+
|
|
54
|
+
this.tui = tui;
|
|
55
|
+
this.authStorage = authStorage;
|
|
56
|
+
this.modelRegistry = modelRegistry;
|
|
57
|
+
this.onDone = onDone;
|
|
58
|
+
this.onDiscover = onDiscover;
|
|
59
|
+
|
|
60
|
+
// Header
|
|
61
|
+
this.addChild(new Text(theme.fg("accent", "Provider Manager"), 0, 0));
|
|
62
|
+
this.addChild(new Spacer(1));
|
|
63
|
+
|
|
64
|
+
// Hints
|
|
65
|
+
const hints = [
|
|
66
|
+
rawKeyHint("d", "discover"),
|
|
67
|
+
rawKeyHint("r", "remove auth"),
|
|
68
|
+
rawKeyHint("esc", "close"),
|
|
69
|
+
].join(" ");
|
|
70
|
+
this.addChild(new Text(hints, 0, 0));
|
|
71
|
+
this.addChild(new Spacer(1));
|
|
72
|
+
|
|
73
|
+
// List
|
|
74
|
+
this.listContainer = new Container();
|
|
75
|
+
this.addChild(this.listContainer);
|
|
76
|
+
|
|
77
|
+
this.loadProviders();
|
|
78
|
+
this.updateList();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private loadProviders(): void {
|
|
82
|
+
const discoverableSet = new Set(getDiscoverableProviders());
|
|
83
|
+
const allModels = this.modelRegistry.getAll();
|
|
84
|
+
|
|
85
|
+
// Group models by provider
|
|
86
|
+
const providerModelCounts = new Map<string, number>();
|
|
87
|
+
for (const model of allModels) {
|
|
88
|
+
providerModelCounts.set(model.provider, (providerModelCounts.get(model.provider) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Build provider list from all known providers
|
|
92
|
+
const providerNames = new Set([
|
|
93
|
+
...providerModelCounts.keys(),
|
|
94
|
+
...discoverableSet,
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
this.providers = Array.from(providerNames)
|
|
98
|
+
.sort()
|
|
99
|
+
.map((name) => ({
|
|
100
|
+
name,
|
|
101
|
+
hasAuth: this.authStorage.hasAuth(name),
|
|
102
|
+
supportsDiscovery: discoverableSet.has(name),
|
|
103
|
+
modelCount: providerModelCounts.get(name) ?? 0,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private updateList(): void {
|
|
108
|
+
this.listContainer.clear();
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < this.providers.length; i++) {
|
|
111
|
+
const p = this.providers[i];
|
|
112
|
+
const isSelected = i === this.selectedIndex;
|
|
113
|
+
|
|
114
|
+
const authBadge = p.hasAuth ? theme.fg("success", "[auth]") : theme.fg("muted", "[no auth]");
|
|
115
|
+
const discoveryBadge = p.supportsDiscovery ? theme.fg("accent", "[discovery]") : "";
|
|
116
|
+
const countBadge = theme.fg("muted", `(${p.modelCount} models)`);
|
|
117
|
+
|
|
118
|
+
const prefix = isSelected ? theme.fg("accent", "> ") : " ";
|
|
119
|
+
const nameText = isSelected ? theme.fg("accent", p.name) : p.name;
|
|
120
|
+
|
|
121
|
+
const parts = [prefix, nameText, " ", authBadge];
|
|
122
|
+
if (discoveryBadge) parts.push(" ", discoveryBadge);
|
|
123
|
+
parts.push(" ", countBadge);
|
|
124
|
+
|
|
125
|
+
this.listContainer.addChild(new Text(parts.join(""), 0, 0));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (this.providers.length === 0) {
|
|
129
|
+
this.listContainer.addChild(new Text(theme.fg("muted", " No providers configured"), 0, 0));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
handleInput(keyData: string): void {
|
|
134
|
+
const kb = getEditorKeybindings();
|
|
135
|
+
|
|
136
|
+
if (kb.matches(keyData, "selectUp")) {
|
|
137
|
+
if (this.providers.length === 0) return;
|
|
138
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.providers.length - 1 : this.selectedIndex - 1;
|
|
139
|
+
this.updateList();
|
|
140
|
+
this.tui.requestRender();
|
|
141
|
+
} else if (kb.matches(keyData, "selectDown")) {
|
|
142
|
+
if (this.providers.length === 0) return;
|
|
143
|
+
this.selectedIndex = this.selectedIndex === this.providers.length - 1 ? 0 : this.selectedIndex + 1;
|
|
144
|
+
this.updateList();
|
|
145
|
+
this.tui.requestRender();
|
|
146
|
+
} else if (kb.matches(keyData, "selectCancel")) {
|
|
147
|
+
this.onDone();
|
|
148
|
+
} else if (keyData === "d" || keyData === "D") {
|
|
149
|
+
const provider = this.providers[this.selectedIndex];
|
|
150
|
+
if (provider?.supportsDiscovery) {
|
|
151
|
+
this.onDiscover(provider.name);
|
|
152
|
+
}
|
|
153
|
+
} else if (keyData === "r" || keyData === "R") {
|
|
154
|
+
const provider = this.providers[this.selectedIndex];
|
|
155
|
+
if (provider?.hasAuth) {
|
|
156
|
+
this.authStorage.remove(provider.name);
|
|
157
|
+
this.loadProviders();
|
|
158
|
+
this.updateList();
|
|
159
|
+
this.tui.requestRender();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -83,6 +83,7 @@ import { appKey, appKeyHint, editorKey, formatKeyForDisplay, keyHint, rawKeyHint
|
|
|
83
83
|
import { LoginDialogComponent } from "./components/login-dialog.js";
|
|
84
84
|
import { ModelSelectorComponent } from "./components/model-selector.js";
|
|
85
85
|
import { OAuthSelectorComponent } from "./components/oauth-selector.js";
|
|
86
|
+
import { ProviderManagerComponent } from "./components/provider-manager.js";
|
|
86
87
|
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
|
|
87
88
|
import { SessionSelectorComponent } from "./components/session-selector.js";
|
|
88
89
|
import { SelectSubmenu, SettingsSelectorComponent, THINKING_DESCRIPTIONS } from "./components/settings-selector.js";
|
|
@@ -1997,6 +1998,11 @@ export class InteractiveMode {
|
|
|
1997
1998
|
this.editor.setText("");
|
|
1998
1999
|
return;
|
|
1999
2000
|
}
|
|
2001
|
+
if (text === "/provider") {
|
|
2002
|
+
this.showProviderManager();
|
|
2003
|
+
this.editor.setText("");
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2000
2006
|
if (text === "/login") {
|
|
2001
2007
|
this.showOAuthSelector("login");
|
|
2002
2008
|
this.editor.setText("");
|
|
@@ -3746,6 +3752,37 @@ export class InteractiveMode {
|
|
|
3746
3752
|
this.showStatus("Resumed session");
|
|
3747
3753
|
}
|
|
3748
3754
|
|
|
3755
|
+
private showProviderManager(): void {
|
|
3756
|
+
this.showSelector((done) => {
|
|
3757
|
+
const component = new ProviderManagerComponent(
|
|
3758
|
+
this.ui,
|
|
3759
|
+
this.session.modelRegistry.authStorage,
|
|
3760
|
+
this.session.modelRegistry,
|
|
3761
|
+
() => {
|
|
3762
|
+
done();
|
|
3763
|
+
this.ui.requestRender();
|
|
3764
|
+
},
|
|
3765
|
+
async (provider: string) => {
|
|
3766
|
+
this.showStatus(`Discovering models for ${provider}...`);
|
|
3767
|
+
try {
|
|
3768
|
+
const results = await this.session.modelRegistry.discoverModels([provider]);
|
|
3769
|
+
const result = results[0];
|
|
3770
|
+
if (result?.error) {
|
|
3771
|
+
this.showError(`Discovery failed: ${result.error}`);
|
|
3772
|
+
} else {
|
|
3773
|
+
this.showStatus(`Discovered ${result?.models.length ?? 0} models from ${provider}`);
|
|
3774
|
+
}
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
3777
|
+
}
|
|
3778
|
+
done();
|
|
3779
|
+
this.ui.requestRender();
|
|
3780
|
+
},
|
|
3781
|
+
);
|
|
3782
|
+
return { component, focus: component };
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3749
3786
|
private async showOAuthSelector(mode: "login" | "logout"): Promise<void> {
|
|
3750
3787
|
if (mode === "logout") {
|
|
3751
3788
|
const providers = this.session.modelRegistry.authStorage.list();
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Diagnostic extraction is handled by session-forensics.ts.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync, openSync, closeSync, constants } from "node:fs";
|
|
11
|
+
import { writeFileSync, writeSync, mkdirSync, readdirSync, unlinkSync, statSync, openSync, closeSync, constants } from "node:fs";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
|
|
@@ -23,6 +23,15 @@ interface ActivityLogState {
|
|
|
23
23
|
|
|
24
24
|
const activityLogState = new Map<string, ActivityLogState>();
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Clear accumulated activity log state (#611).
|
|
28
|
+
* Call when auto-mode stops to prevent unbounded memory growth
|
|
29
|
+
* from lastSnapshotKeyByUnit maps accumulating across units.
|
|
30
|
+
*/
|
|
31
|
+
export function clearActivityLogState(): void {
|
|
32
|
+
activityLogState.clear();
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
function scanNextSequence(activityDir: string): number {
|
|
27
36
|
let maxSeq = 0;
|
|
28
37
|
try {
|
|
@@ -46,9 +55,21 @@ function getActivityState(activityDir: string): ActivityLogState {
|
|
|
46
55
|
return state;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Build a lightweight dedup key from session entries without serializing
|
|
60
|
+
* the entire content to a string (#611). Uses entry count + hash of
|
|
61
|
+
* the last few entries as a fingerprint instead of hashing megabytes.
|
|
62
|
+
*/
|
|
63
|
+
function snapshotKey(unitType: string, unitId: string, entries: unknown[]): string {
|
|
64
|
+
const hash = createHash("sha1");
|
|
65
|
+
hash.update(`${unitType}\0${unitId}\0${entries.length}\0`);
|
|
66
|
+
// Hash only the last 3 entries as a fingerprint — if the session grew,
|
|
67
|
+
// the count change alone detects it; if content changed, the tail hash catches it.
|
|
68
|
+
const tail = entries.slice(-3);
|
|
69
|
+
for (const entry of tail) {
|
|
70
|
+
hash.update(JSON.stringify(entry));
|
|
71
|
+
}
|
|
72
|
+
return hash.digest("hex");
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
function nextActivityFilePath(
|
|
@@ -91,14 +112,23 @@ export function saveActivityLog(
|
|
|
91
112
|
mkdirSync(activityDir, { recursive: true });
|
|
92
113
|
|
|
93
114
|
const safeUnitId = unitId.replace(/\//g, "-");
|
|
94
|
-
const content = `${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`;
|
|
95
115
|
const state = getActivityState(activityDir);
|
|
96
116
|
const unitKey = `${unitType}\0${safeUnitId}`;
|
|
97
|
-
|
|
117
|
+
// Use lightweight fingerprint instead of serializing all entries (#611)
|
|
118
|
+
const key = snapshotKey(unitType, safeUnitId, entries);
|
|
98
119
|
if (state.lastSnapshotKeyByUnit.get(unitKey) === key) return;
|
|
99
120
|
|
|
100
121
|
const filePath = nextActivityFilePath(activityDir, state, unitType, safeUnitId);
|
|
101
|
-
|
|
122
|
+
// Stream entries to disk line-by-line instead of building one massive string (#611).
|
|
123
|
+
// For large sessions, the single-string approach allocated hundreds of MB.
|
|
124
|
+
const fd = openSync(filePath, "w");
|
|
125
|
+
try {
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
writeSync(fd, JSON.stringify(entry) + "\n");
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
closeSync(fd);
|
|
131
|
+
}
|
|
102
132
|
state.nextSeq += 1;
|
|
103
133
|
state.lastSnapshotKeyByUnit.set(unitKey, key);
|
|
104
134
|
} catch (e) {
|
|
@@ -10,7 +10,7 @@ import type { ExtensionContext, ExtensionCommandContext } from "@gsd/pi-coding-a
|
|
|
10
10
|
import type { GSDState } from "./types.js";
|
|
11
11
|
import { getCurrentBranch } from "./worktree.js";
|
|
12
12
|
import { getActiveHook } from "./post-unit-hooks.js";
|
|
13
|
-
import { getLedger, getProjectTotals, formatCost, formatTokenCount } from "./metrics.js";
|
|
13
|
+
import { getLedger, getProjectTotals, formatCost, formatTokenCount, formatTierSavings } from "./metrics.js";
|
|
14
14
|
import {
|
|
15
15
|
resolveMilestoneFile,
|
|
16
16
|
resolveSliceFile,
|
|
@@ -39,6 +39,8 @@ export interface AutoDashboardData {
|
|
|
39
39
|
projectedRemainingCost?: number;
|
|
40
40
|
/** Whether token profile has been auto-downgraded due to budget prediction */
|
|
41
41
|
profileDowngraded?: boolean;
|
|
42
|
+
/** Number of pending captures awaiting triage (0 if none or file missing) */
|
|
43
|
+
pendingCaptureCount: number;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// ─── Unit Description Helpers ─────────────────────────────────────────────────
|
|
@@ -239,6 +241,7 @@ export function updateProgressWidget(
|
|
|
239
241
|
unitId: string,
|
|
240
242
|
state: GSDState,
|
|
241
243
|
accessors: WidgetStateAccessors,
|
|
244
|
+
tierBadge?: string,
|
|
242
245
|
): void {
|
|
243
246
|
if (!ctx.hasUI) return;
|
|
244
247
|
|
|
@@ -319,7 +322,8 @@ export function updateProgressWidget(
|
|
|
319
322
|
|
|
320
323
|
const target = task ? `${task.id}: ${task.title}` : unitId;
|
|
321
324
|
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
|
|
322
|
-
const
|
|
325
|
+
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
|
|
326
|
+
const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
|
|
323
327
|
lines.push(rightAlign(actionLeft, phaseBadge, width));
|
|
324
328
|
lines.push("");
|
|
325
329
|
|
|
@@ -414,6 +418,14 @@ export function updateProgressWidget(
|
|
|
414
418
|
? `${modelPhase}${theme.fg("dim", modelDisplay)}`
|
|
415
419
|
: "";
|
|
416
420
|
lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
|
|
421
|
+
|
|
422
|
+
// Dynamic routing savings summary
|
|
423
|
+
if (mLedger && mLedger.units.some(u => u.tier)) {
|
|
424
|
+
const savings = formatTierSavings(mLedger.units);
|
|
425
|
+
if (savings) {
|
|
426
|
+
lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
417
429
|
}
|
|
418
430
|
|
|
419
431
|
const hintParts: string[] = [];
|