@hadooppei/hwcode 0.1.0 → 0.2.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/.pi/extensions/cloud.ts +587 -0
- package/.pi/extensions/command-filter.ts +2 -6
- package/.pi/extensions/context-policy.ts +58 -0
- package/.pi/extensions/cwd.ts +12 -9
- package/.pi/extensions/model-providers.ts +92 -150
- package/.pi/extensions/welcome.ts +25 -9
- package/.pi/extensions/workflows.ts +41 -77
- package/.pi/lib/cloud/adapters.ts +234 -0
- package/.pi/lib/cloud/process.ts +91 -0
- package/.pi/lib/cloud/templates.ts +148 -0
- package/.pi/lib/cloud-providers.ts +263 -0
- package/.pi/lib/cloud-vault.ts +209 -0
- package/.pi/lib/command-filter.ts +1 -0
- package/.pi/lib/context/compaction.ts +113 -0
- package/.pi/lib/context-policy.ts +120 -0
- package/.pi/lib/models/provider-config.ts +68 -0
- package/.pi/lib/models/readiness.ts +18 -0
- package/.pi/lib/pixel-font.ts +5 -1
- package/.pi/lib/runtime/config.ts +70 -0
- package/.pi/lib/runtime/session-state.ts +61 -0
- package/.pi/lib/workflows/state.ts +138 -0
- package/.pi/lib/working-directory.ts +7 -7
- package/.pi/lib/workspace/access-policy.ts +49 -0
- package/.pi/model-providers.json +13 -5
- package/.pi/settings.json +41 -0
- package/.pi/skills/hwcode-cloud/SKILL.md +91 -0
- package/.pi/skills/hwcode-cloud/agents/openai.yaml +4 -0
- package/.pi/welcome.json +4 -2
- package/README.md +103 -1
- package/bin/hwcode.js +76 -4
- package/package.json +8 -7
package/.pi/extensions/cwd.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
canonicalizeDirectory,
|
|
10
|
-
|
|
10
|
+
clearWorkingDirectoryState,
|
|
11
11
|
getActiveWorkflowRoot,
|
|
12
12
|
getWorkingDirectory,
|
|
13
13
|
getWorkingDirectoryState,
|
|
@@ -20,9 +20,8 @@ import {
|
|
|
20
20
|
type WorkingDirectoryState,
|
|
21
21
|
WORKING_DIRECTORY_STATE_TYPE,
|
|
22
22
|
} from "../lib/working-directory.ts";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const OPTIONAL_PATH_TOOLS = new Set(["grep", "find", "ls"]);
|
|
23
|
+
import { isPathInsideRoot } from "../lib/workflow-guard.ts";
|
|
24
|
+
import { FILE_PATH_TOOLS, OPTIONAL_PATH_TOOLS } from "../lib/workspace/access-policy.ts";
|
|
26
25
|
|
|
27
26
|
interface ChangeResult {
|
|
28
27
|
state?: WorkingDirectoryState;
|
|
@@ -60,7 +59,7 @@ export default function workingDirectoryExtension(pi: ExtensionAPI) {
|
|
|
60
59
|
} catch {
|
|
61
60
|
// The workflow guard will handle a missing locked root separately.
|
|
62
61
|
}
|
|
63
|
-
if (target
|
|
62
|
+
if (!isPathInsideRoot(canonicalWorkflowRoot, target)) {
|
|
64
63
|
return {
|
|
65
64
|
error: `The active HWCode workflow locks this session to ${workflowRoot}. Start a new session before switching to ${target}.`,
|
|
66
65
|
};
|
|
@@ -119,7 +118,7 @@ export default function workingDirectoryExtension(pi: ExtensionAPI) {
|
|
|
119
118
|
`\nCurrent working directory: ${cwd}`,
|
|
120
119
|
);
|
|
121
120
|
return {
|
|
122
|
-
systemPrompt: `${prompt}\
|
|
121
|
+
systemPrompt: `${prompt}\nOnly a leading shell \`cd\` changes the persistent working directory for this session. Conditional or later \`cd\` commands remain local to that shell process. All relative shell and built-in file-tool paths resolve from the current execution directory. Changing it does not reload Pi project resources; start a new session from another project when its AGENTS.md, settings, or skills must be loaded.`,
|
|
123
122
|
};
|
|
124
123
|
});
|
|
125
124
|
|
|
@@ -127,7 +126,7 @@ export default function workingDirectoryExtension(pi: ExtensionAPI) {
|
|
|
127
126
|
const input = event.input as Record<string, unknown>;
|
|
128
127
|
if (event.toolName === "bash" && typeof input.command === "string") {
|
|
129
128
|
const executionCwd = getWorkingDirectory(ctx.sessionManager);
|
|
130
|
-
const change =
|
|
129
|
+
const change = parseLeadingDirectoryChange(input.command);
|
|
131
130
|
if (change) {
|
|
132
131
|
const result = changeDirectory(change.argument, ctx);
|
|
133
132
|
if (result.error) return { block: true, reason: result.error };
|
|
@@ -149,7 +148,7 @@ export default function workingDirectoryExtension(pi: ExtensionAPI) {
|
|
|
149
148
|
|
|
150
149
|
pi.on("user_bash", (event, ctx) => {
|
|
151
150
|
const executionCwd = getWorkingDirectory(ctx.sessionManager);
|
|
152
|
-
const change =
|
|
151
|
+
const change = parseLeadingDirectoryChange(event.command);
|
|
153
152
|
if (change) {
|
|
154
153
|
const result = changeDirectory(change.argument, ctx);
|
|
155
154
|
if (result.error) {
|
|
@@ -162,7 +161,7 @@ export default function workingDirectoryExtension(pi: ExtensionAPI) {
|
|
|
162
161
|
},
|
|
163
162
|
};
|
|
164
163
|
}
|
|
165
|
-
if (change.
|
|
164
|
+
if (!change.remainder) {
|
|
166
165
|
return {
|
|
167
166
|
result: {
|
|
168
167
|
output: `${result.state!.cwd}\n`,
|
|
@@ -185,4 +184,8 @@ export default function workingDirectoryExtension(pi: ExtensionAPI) {
|
|
|
185
184
|
};
|
|
186
185
|
return { operations };
|
|
187
186
|
});
|
|
187
|
+
|
|
188
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
189
|
+
clearWorkingDirectoryState(ctx.sessionManager);
|
|
190
|
+
});
|
|
188
191
|
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
2
|
import type {
|
|
6
3
|
ApiKeyCredential,
|
|
@@ -11,145 +8,35 @@ import type {
|
|
|
11
8
|
} from "@earendil-works/pi-ai";
|
|
12
9
|
import { stream, streamSimple } from "@earendil-works/pi-ai/compat";
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
input?: ModelInput[];
|
|
29
|
-
contextWindow?: number;
|
|
30
|
-
maxTokens?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface LoginConfig {
|
|
34
|
-
enabled: true;
|
|
35
|
-
promptBaseUrl?: boolean;
|
|
36
|
-
promptApiKey?: boolean;
|
|
37
|
-
apiKeyRequired?: boolean;
|
|
38
|
-
catalogPath?: string;
|
|
39
|
-
timeoutMs?: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface ModelProviderConfig {
|
|
43
|
-
id: string;
|
|
44
|
-
name?: string;
|
|
45
|
-
baseUrl?: string;
|
|
46
|
-
baseUrlEnv?: string;
|
|
47
|
-
apiKeyEnv?: string;
|
|
48
|
-
login?: LoginConfig;
|
|
49
|
-
modelDefaults?: ModelDefaults;
|
|
50
|
-
models?: ConfiguredModel[];
|
|
51
|
-
}
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_CONTEXT_POLICY,
|
|
13
|
+
loadContextPolicy,
|
|
14
|
+
resolveContextWindow,
|
|
15
|
+
type ContextPolicy,
|
|
16
|
+
} from "../lib/context-policy.ts";
|
|
17
|
+
import {
|
|
18
|
+
isPositiveNumber,
|
|
19
|
+
isRecord,
|
|
20
|
+
loadModelProvidersConfig,
|
|
21
|
+
normalizeBaseUrl,
|
|
22
|
+
type ModelInput,
|
|
23
|
+
type ModelProviderConfig,
|
|
24
|
+
} from "../lib/models/provider-config.ts";
|
|
52
25
|
|
|
53
|
-
|
|
54
|
-
providers: ModelProviderConfig[];
|
|
55
|
-
}
|
|
26
|
+
type OpenAIModel = Model<"openai-completions">;
|
|
56
27
|
|
|
57
28
|
interface RemoteModel {
|
|
58
29
|
id: string;
|
|
59
30
|
name?: string;
|
|
60
31
|
input?: ModelInput[];
|
|
32
|
+
contextWindow?: number;
|
|
61
33
|
}
|
|
62
34
|
|
|
63
|
-
const BUNDLED_CONFIG_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "model-providers.json");
|
|
64
35
|
const CREDENTIAL_BASE_URL = "BASE_URL";
|
|
65
|
-
const DEFAULT_CONTEXT_WINDOW = 32768;
|
|
66
36
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
67
37
|
const DEFAULT_CATALOG_PATH = "models";
|
|
68
38
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
69
39
|
|
|
70
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
71
|
-
return typeof value === "object" && value !== null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function isPositiveNumber(value: unknown): value is number {
|
|
75
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function normalizeBaseUrl(value: string): string {
|
|
79
|
-
const url = new URL(value.trim());
|
|
80
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
81
|
-
throw new Error("Provider base URL must use http or https");
|
|
82
|
-
}
|
|
83
|
-
url.hash = "";
|
|
84
|
-
url.search = "";
|
|
85
|
-
return url.toString().replace(/\/$/u, "");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function validateModel(providerId: string, model: ConfiguredModel): void {
|
|
89
|
-
if (!model.id?.trim()) {
|
|
90
|
-
throw new Error(`Provider "${providerId}" contains a model without an id`);
|
|
91
|
-
}
|
|
92
|
-
if (model.input?.some((input) => input !== "text" && input !== "image")) {
|
|
93
|
-
throw new Error(`Model "${model.id}" has an unsupported input type`);
|
|
94
|
-
}
|
|
95
|
-
if (model.contextWindow !== undefined && !isPositiveNumber(model.contextWindow)) {
|
|
96
|
-
throw new Error(`Model "${model.id}" has an invalid contextWindow`);
|
|
97
|
-
}
|
|
98
|
-
if (model.maxTokens !== undefined && !isPositiveNumber(model.maxTokens)) {
|
|
99
|
-
throw new Error(`Model "${model.id}" has an invalid maxTokens`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function loadConfig(): ModelProvidersConfig {
|
|
104
|
-
const projectConfigPath = resolve(process.cwd(), ".pi/model-providers.json");
|
|
105
|
-
const profileConfigPath = process.env.HWCODE_PROFILE_DIR
|
|
106
|
-
? join(process.env.HWCODE_PROFILE_DIR, "model-providers.json")
|
|
107
|
-
: BUNDLED_CONFIG_PATH;
|
|
108
|
-
const configPath = existsSync(projectConfigPath) ? projectConfigPath : profileConfigPath;
|
|
109
|
-
const config = JSON.parse(readFileSync(configPath, "utf8")) as ModelProvidersConfig;
|
|
110
|
-
if (!Array.isArray(config.providers) || config.providers.length === 0) {
|
|
111
|
-
throw new Error(`${configPath} must contain a non-empty providers array`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const providerIds = new Set<string>();
|
|
115
|
-
for (const provider of config.providers) {
|
|
116
|
-
if (!provider.id?.trim()) {
|
|
117
|
-
throw new Error(`${configPath} contains a provider without an id`);
|
|
118
|
-
}
|
|
119
|
-
if (providerIds.has(provider.id)) {
|
|
120
|
-
throw new Error(`${configPath} contains duplicate provider id "${provider.id}"`);
|
|
121
|
-
}
|
|
122
|
-
providerIds.add(provider.id);
|
|
123
|
-
|
|
124
|
-
if (provider.baseUrl) normalizeBaseUrl(provider.baseUrl);
|
|
125
|
-
if (!provider.login?.enabled && !provider.baseUrl?.trim()) {
|
|
126
|
-
throw new Error(`Static provider "${provider.id}" requires baseUrl`);
|
|
127
|
-
}
|
|
128
|
-
if (!provider.login?.enabled && (!Array.isArray(provider.models) || provider.models.length === 0)) {
|
|
129
|
-
throw new Error(`Static provider "${provider.id}" must contain at least one model`);
|
|
130
|
-
}
|
|
131
|
-
if (provider.login?.enabled && provider.login.promptBaseUrl === false && !provider.baseUrl?.trim()) {
|
|
132
|
-
throw new Error(`Login provider "${provider.id}" requires baseUrl when promptBaseUrl is false`);
|
|
133
|
-
}
|
|
134
|
-
if (provider.login?.catalogPath !== undefined && !provider.login.catalogPath.trim()) {
|
|
135
|
-
throw new Error(`Login provider "${provider.id}" has an empty catalogPath`);
|
|
136
|
-
}
|
|
137
|
-
if (provider.login?.timeoutMs !== undefined && !isPositiveNumber(provider.login.timeoutMs)) {
|
|
138
|
-
throw new Error(`Login provider "${provider.id}" has an invalid timeoutMs`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const modelIds = new Set<string>();
|
|
142
|
-
for (const model of provider.models ?? []) {
|
|
143
|
-
validateModel(provider.id, model);
|
|
144
|
-
if (modelIds.has(model.id)) {
|
|
145
|
-
throw new Error(`Provider "${provider.id}" contains duplicate model id "${model.id}"`);
|
|
146
|
-
}
|
|
147
|
-
modelIds.add(model.id);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return config;
|
|
152
|
-
}
|
|
153
40
|
|
|
154
41
|
function compatibility() {
|
|
155
42
|
return {
|
|
@@ -165,10 +52,15 @@ function toPiModel(
|
|
|
165
52
|
provider: ModelProviderConfig,
|
|
166
53
|
baseUrl: string,
|
|
167
54
|
remote: RemoteModel,
|
|
55
|
+
contextPolicy: ContextPolicy,
|
|
168
56
|
): OpenAIModel {
|
|
169
57
|
const override = provider.models?.find((model) => model.id === remote.id);
|
|
170
58
|
const defaults = provider.modelDefaults;
|
|
171
|
-
const contextWindow =
|
|
59
|
+
const contextWindow = resolveContextWindow(
|
|
60
|
+
override?.contextWindow ?? defaults?.contextWindow,
|
|
61
|
+
remote.contextWindow,
|
|
62
|
+
contextPolicy,
|
|
63
|
+
);
|
|
172
64
|
const maxTokens = override?.maxTokens ?? defaults?.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
173
65
|
|
|
174
66
|
return {
|
|
@@ -196,6 +88,22 @@ function remoteInput(model: Record<string, unknown>): ModelInput[] | undefined {
|
|
|
196
88
|
return input;
|
|
197
89
|
}
|
|
198
90
|
|
|
91
|
+
function remoteContextWindow(model: Record<string, unknown>): number | undefined {
|
|
92
|
+
const architecture = isRecord(model.architecture) ? model.architecture : undefined;
|
|
93
|
+
for (const value of [
|
|
94
|
+
model.context_window,
|
|
95
|
+
model.context_length,
|
|
96
|
+
model.max_context_length,
|
|
97
|
+
model.max_model_len,
|
|
98
|
+
model.n_ctx,
|
|
99
|
+
architecture?.context_window,
|
|
100
|
+
architecture?.context_length,
|
|
101
|
+
]) {
|
|
102
|
+
if (isPositiveNumber(value)) return Math.floor(value);
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
199
107
|
function parseRemoteModels(payload: unknown): RemoteModel[] {
|
|
200
108
|
const entries = Array.isArray(payload)
|
|
201
109
|
? payload
|
|
@@ -215,6 +123,7 @@ function parseRemoteModels(payload: unknown): RemoteModel[] {
|
|
|
215
123
|
id,
|
|
216
124
|
name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined,
|
|
217
125
|
input: remoteInput(entry),
|
|
126
|
+
contextWindow: remoteContextWindow(entry),
|
|
218
127
|
});
|
|
219
128
|
}
|
|
220
129
|
|
|
@@ -235,6 +144,7 @@ async function discoverModels(
|
|
|
235
144
|
baseUrl: string,
|
|
236
145
|
apiKey: string | undefined,
|
|
237
146
|
signal: AbortSignal,
|
|
147
|
+
contextPolicy: ContextPolicy,
|
|
238
148
|
): Promise<OpenAIModel[]> {
|
|
239
149
|
const login = provider.login;
|
|
240
150
|
if (!login) throw new Error(`Provider "${provider.id}" has no login configuration`);
|
|
@@ -259,7 +169,7 @@ async function discoverModels(
|
|
|
259
169
|
throw new Error(`Model discovery failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
260
170
|
}
|
|
261
171
|
|
|
262
|
-
return parseRemoteModels(payload).map((model) => toPiModel(provider, baseUrl, model));
|
|
172
|
+
return parseRemoteModels(payload).map((model) => toPiModel(provider, baseUrl, model, contextPolicy));
|
|
263
173
|
}
|
|
264
174
|
|
|
265
175
|
async function environmentValue(name: string | undefined): Promise<string | undefined> {
|
|
@@ -287,7 +197,11 @@ async function configuredBaseUrl(
|
|
|
287
197
|
return provider.baseUrl ? normalizeBaseUrl(provider.baseUrl) : undefined;
|
|
288
198
|
}
|
|
289
199
|
|
|
290
|
-
function registerStaticProvider(
|
|
200
|
+
function registerStaticProvider(
|
|
201
|
+
pi: ExtensionAPI,
|
|
202
|
+
config: ModelProviderConfig,
|
|
203
|
+
contextPolicy: ContextPolicy,
|
|
204
|
+
): void {
|
|
291
205
|
const baseUrl = normalizeBaseUrl(config.baseUrl!);
|
|
292
206
|
const apiKey = config.apiKeyEnv
|
|
293
207
|
? process.env[config.apiKeyEnv]?.trim() || "local"
|
|
@@ -299,21 +213,32 @@ function registerStaticProvider(pi: ExtensionAPI, config: ModelProviderConfig):
|
|
|
299
213
|
api: "openai-completions",
|
|
300
214
|
apiKey,
|
|
301
215
|
compat: compatibility(),
|
|
302
|
-
models: (config.models ?? []).map((model) =>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
216
|
+
models: (config.models ?? []).map((model) => {
|
|
217
|
+
const contextWindow = resolveContextWindow(
|
|
218
|
+
model.contextWindow ?? config.modelDefaults?.contextWindow,
|
|
219
|
+
undefined,
|
|
220
|
+
contextPolicy,
|
|
221
|
+
);
|
|
222
|
+
return {
|
|
223
|
+
id: model.id,
|
|
224
|
+
name: model.name ?? model.id,
|
|
225
|
+
reasoning: model.reasoning ?? config.modelDefaults?.reasoning ?? false,
|
|
226
|
+
input: model.input ?? config.modelDefaults?.input ?? ["text"],
|
|
227
|
+
contextWindow,
|
|
228
|
+
maxTokens: Math.min(
|
|
229
|
+
model.maxTokens ?? config.modelDefaults?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
230
|
+
contextWindow,
|
|
231
|
+
),
|
|
232
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
233
|
+
};
|
|
234
|
+
}),
|
|
311
235
|
});
|
|
312
236
|
}
|
|
313
237
|
|
|
314
238
|
async function loginAndDiscover(
|
|
315
239
|
config: ModelProviderConfig,
|
|
316
240
|
interaction: ProviderAuthInteraction,
|
|
241
|
+
contextPolicy: ContextPolicy,
|
|
317
242
|
): Promise<{ credential: ApiKeyCredential; models: OpenAIModel[] }> {
|
|
318
243
|
const login = config.login!;
|
|
319
244
|
try {
|
|
@@ -343,7 +268,7 @@ async function loginAndDiscover(
|
|
|
343
268
|
}
|
|
344
269
|
|
|
345
270
|
interaction.notify({ type: "progress", message: `Discovering models from ${baseUrl}` });
|
|
346
|
-
const models = await discoverModels(config, baseUrl, apiKey, interaction.signal);
|
|
271
|
+
const models = await discoverModels(config, baseUrl, apiKey, interaction.signal, contextPolicy);
|
|
347
272
|
interaction.notify({ type: "info", message: `Found ${models.length} model${models.length === 1 ? "" : "s"}.` });
|
|
348
273
|
|
|
349
274
|
return {
|
|
@@ -362,7 +287,10 @@ async function loginAndDiscover(
|
|
|
362
287
|
}
|
|
363
288
|
}
|
|
364
289
|
|
|
365
|
-
function createLoginProvider(
|
|
290
|
+
function createLoginProvider(
|
|
291
|
+
config: ModelProviderConfig,
|
|
292
|
+
contextPolicy: ContextPolicy,
|
|
293
|
+
): Provider<"openai-completions"> {
|
|
366
294
|
const login = config.login!;
|
|
367
295
|
const displayName = config.name ?? config.id;
|
|
368
296
|
let models: OpenAIModel[] = config.baseUrl
|
|
@@ -370,12 +298,15 @@ function createLoginProvider(config: ModelProviderConfig): Provider<"openai-comp
|
|
|
370
298
|
config,
|
|
371
299
|
normalizeBaseUrl(config.baseUrl!),
|
|
372
300
|
{ id: model.id, name: model.name, input: model.input },
|
|
301
|
+
contextPolicy,
|
|
373
302
|
))
|
|
374
303
|
: [];
|
|
375
304
|
|
|
376
305
|
const ambientApiKey = () => environmentValue(config.apiKeyEnv);
|
|
377
306
|
const ambientBaseUrl = () => environmentValue(config.baseUrlEnv);
|
|
378
|
-
const hasAmbientConfiguration = async () => Boolean(
|
|
307
|
+
const hasAmbientConfiguration = async () => Boolean(
|
|
308
|
+
config.baseUrl || await ambientApiKey() || await ambientBaseUrl()
|
|
309
|
+
);
|
|
379
310
|
|
|
380
311
|
return {
|
|
381
312
|
id: config.id,
|
|
@@ -385,7 +316,7 @@ function createLoginProvider(config: ModelProviderConfig): Provider<"openai-comp
|
|
|
385
316
|
apiKey: {
|
|
386
317
|
name: `${displayName} credentials`,
|
|
387
318
|
login: async (interaction: ProviderAuthInteraction) => {
|
|
388
|
-
const result = await loginAndDiscover(config, interaction);
|
|
319
|
+
const result = await loginAndDiscover(config, interaction, contextPolicy);
|
|
389
320
|
models = result.models;
|
|
390
321
|
return result.credential;
|
|
391
322
|
},
|
|
@@ -416,7 +347,12 @@ function createLoginProvider(config: ModelProviderConfig): Provider<"openai-comp
|
|
|
416
347
|
if (context.stored) {
|
|
417
348
|
const restored = context.stored.models.filter(
|
|
418
349
|
(model): model is OpenAIModel => model.provider === config.id && model.api === "openai-completions",
|
|
419
|
-
)
|
|
350
|
+
).map((model) => {
|
|
351
|
+
const configured = config.models?.find((item) => item.id === model.id)?.contextWindow
|
|
352
|
+
?? config.modelDefaults?.contextWindow;
|
|
353
|
+
const contextWindow = resolveContextWindow(configured, model.contextWindow, contextPolicy);
|
|
354
|
+
return { ...model, contextWindow, maxTokens: Math.min(model.maxTokens, contextWindow) };
|
|
355
|
+
});
|
|
420
356
|
if (!await context.publish({ update: () => { models = restored; } })) return;
|
|
421
357
|
}
|
|
422
358
|
if (!context.allowNetwork || context.signal.aborted) return;
|
|
@@ -430,7 +366,7 @@ function createLoginProvider(config: ModelProviderConfig): Provider<"openai-comp
|
|
|
430
366
|
const apiKey = credential?.key ?? await ambientApiKey();
|
|
431
367
|
if (!baseUrl || (login.apiKeyRequired && !apiKey)) return;
|
|
432
368
|
|
|
433
|
-
const refreshed = await discoverModels(config, baseUrl, apiKey, context.signal);
|
|
369
|
+
const refreshed = await discoverModels(config, baseUrl, apiKey, context.signal, contextPolicy);
|
|
434
370
|
await context.publish({
|
|
435
371
|
persist: { models: refreshed, checkedAt: Date.now() },
|
|
436
372
|
update: () => { models = refreshed; },
|
|
@@ -442,9 +378,15 @@ function createLoginProvider(config: ModelProviderConfig): Provider<"openai-comp
|
|
|
442
378
|
}
|
|
443
379
|
|
|
444
380
|
export default function modelProvidersExtension(pi: ExtensionAPI) {
|
|
445
|
-
const config =
|
|
381
|
+
const config = loadModelProvidersConfig();
|
|
382
|
+
let contextPolicy = { ...DEFAULT_CONTEXT_POLICY };
|
|
383
|
+
try {
|
|
384
|
+
contextPolicy = loadContextPolicy(process.cwd());
|
|
385
|
+
} catch {
|
|
386
|
+
// The context extension reports malformed settings in the UI. Providers stay usable with safe defaults.
|
|
387
|
+
}
|
|
446
388
|
for (const provider of config.providers) {
|
|
447
|
-
if (provider.login?.enabled) pi.registerProvider(createLoginProvider(provider));
|
|
448
|
-
else registerStaticProvider(pi, provider);
|
|
389
|
+
if (provider.login?.enabled) pi.registerProvider(createLoginProvider(provider, contextPolicy));
|
|
390
|
+
else registerStaticProvider(pi, provider, contextPolicy);
|
|
449
391
|
}
|
|
450
392
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
1
|
import { dirname, join } from "node:path";
|
|
3
2
|
import { fileURLToPath } from "node:url";
|
|
4
3
|
import {
|
|
5
|
-
CONFIG_DIR_NAME,
|
|
6
4
|
type ExtensionAPI,
|
|
7
5
|
type ExtensionContext,
|
|
8
6
|
type ThemeColor,
|
|
9
7
|
} from "@earendil-works/pi-coding-agent";
|
|
10
8
|
import { isThemeColor, renderPixelText } from "../lib/pixel-font.ts";
|
|
9
|
+
import { loadReplacingResource } from "../lib/runtime/config.ts";
|
|
11
10
|
import { shouldDismissWelcomeOnSubmit } from "../lib/welcome-input.ts";
|
|
12
11
|
|
|
13
12
|
interface WelcomeConfig {
|
|
@@ -17,6 +16,8 @@ interface WelcomeConfig {
|
|
|
17
16
|
subtitle: string;
|
|
18
17
|
subtitleColor: ThemeColor;
|
|
19
18
|
reservedRows: number;
|
|
19
|
+
logoScale: number;
|
|
20
|
+
verticalOffsetRows: number;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const DEFAULT_CONFIG: WelcomeConfig = {
|
|
@@ -26,15 +27,17 @@ const DEFAULT_CONFIG: WelcomeConfig = {
|
|
|
26
27
|
subtitle: "Local AI Coding Workspace",
|
|
27
28
|
subtitleColor: "muted",
|
|
28
29
|
reservedRows: 8,
|
|
30
|
+
logoScale: 0.82,
|
|
31
|
+
verticalOffsetRows: 1,
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
function loadConfig(cwd: string): WelcomeConfig {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const fallbackPath = join(dirname(fileURLToPath(import.meta.url)), "..", "welcome.json");
|
|
36
|
+
const raw = loadReplacingResource<Partial<WelcomeConfig> & Record<string, unknown>>(
|
|
37
|
+
cwd,
|
|
38
|
+
"welcome.json",
|
|
39
|
+
fallbackPath,
|
|
40
|
+
);
|
|
38
41
|
const logo = raw.logo?.trim().toUpperCase() ?? DEFAULT_CONFIG.logo;
|
|
39
42
|
|
|
40
43
|
if (!/^[A-Z]{1,6}$/.test(logo)) {
|
|
@@ -59,6 +62,14 @@ function loadConfig(cwd: string): WelcomeConfig {
|
|
|
59
62
|
typeof raw.reservedRows === "number" && raw.reservedRows >= 5
|
|
60
63
|
? Math.floor(raw.reservedRows)
|
|
61
64
|
: DEFAULT_CONFIG.reservedRows,
|
|
65
|
+
logoScale:
|
|
66
|
+
typeof raw.logoScale === "number" && raw.logoScale >= 0.5 && raw.logoScale <= 1
|
|
67
|
+
? raw.logoScale
|
|
68
|
+
: DEFAULT_CONFIG.logoScale,
|
|
69
|
+
verticalOffsetRows:
|
|
70
|
+
typeof raw.verticalOffsetRows === "number" && raw.verticalOffsetRows >= -4 && raw.verticalOffsetRows <= 4
|
|
71
|
+
? Math.trunc(raw.verticalOffsetRows)
|
|
72
|
+
: DEFAULT_CONFIG.verticalOffsetRows,
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
75
|
|
|
@@ -101,9 +112,14 @@ export default function welcomeExtension(pi: ExtensionAPI) {
|
|
|
101
112
|
theme,
|
|
102
113
|
width,
|
|
103
114
|
Math.max(1, availableHeight - subtitleHeight),
|
|
115
|
+
config.logoScale,
|
|
104
116
|
);
|
|
105
117
|
const groupHeight = logo.height + subtitleHeight;
|
|
106
|
-
const
|
|
118
|
+
const centeredTop = Math.max(0, Math.floor((availableHeight - groupHeight) / 2));
|
|
119
|
+
const topPadding = Math.max(
|
|
120
|
+
0,
|
|
121
|
+
Math.min(availableHeight - groupHeight, centeredTop + config.verticalOffsetRows),
|
|
122
|
+
);
|
|
107
123
|
const bottomPadding = Math.max(0, availableHeight - topPadding - groupHeight);
|
|
108
124
|
const lines = [...Array<string>(topPadding).fill(""), ...logo.lines];
|
|
109
125
|
|