privateer-agent 0.5.0 → 0.6.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/bin/privateer-launch.mjs +218 -0
- package/bin/privateer-tui +8 -139
- package/bin/privateer.cmd +6 -0
- package/extensions/privateer-brand.ts +117 -72
- package/extensions/privateer-gate.ts +6 -3
- package/extensions/privateer-models.ts +429 -0
- package/extensions/privateer-posture.ts +11 -14
- package/package.json +6 -3
- package/patches/@earendil-works+pi-coding-agent+0.80.3.patch +23 -0
- package/src/cli/chat.ts +15 -2
- package/src/providers/account.ts +82 -12
- package/src/providers/defaultModel.ts +31 -6
- package/src/ui/palette.ts +135 -0
|
@@ -24,7 +24,9 @@ import { homedir } from "node:os";
|
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import * as priv from "../src/auth/privateer.ts";
|
|
26
26
|
import { makeAccountProvider } from "../src/providers/account.ts";
|
|
27
|
+
import { resolveSignedInModel } from "../src/providers/defaultModel.ts";
|
|
27
28
|
import { discoverContextFiles, onContextChanged } from "../src/context.ts";
|
|
29
|
+
import { type Palette, paletteFor } from "../src/ui/palette.ts";
|
|
28
30
|
|
|
29
31
|
const VERSION: string = (() => {
|
|
30
32
|
try {
|
|
@@ -34,37 +36,26 @@ const VERSION: string = (() => {
|
|
|
34
36
|
}
|
|
35
37
|
})();
|
|
36
38
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
const RESET = `${ESC}0m`;
|
|
45
|
-
const BOLD = `${ESC}1m`;
|
|
46
|
-
const c = (n: number): string => `${ESC}38;5;${n}m`;
|
|
47
|
-
const OCEAN = c(231); // white (#ffffff) — anchor / wordmark "P"
|
|
48
|
-
const OCEAN_LIGHT = c(231); // white (#ffffff) — wordmark, version, path
|
|
49
|
-
const BORDER = c(231); // white (#ffffff) — the frame
|
|
50
|
-
const DIM = `${ESC}90m`;
|
|
51
|
-
const GREEN = `${ESC}32m`;
|
|
52
|
-
const YELLOW = `${ESC}33m`;
|
|
39
|
+
// The banner paints from Pi's ACTIVE theme (paletteFor, in src/ui/palette.ts) rather
|
|
40
|
+
// than a fixed colour. It used to hardcode everything to white (256-color 231) "because
|
|
41
|
+
// navy is too dark on a dark terminal" — which inverts the problem on a LIGHT terminal
|
|
42
|
+
// (white-on-white → the whole mark, wordmark, and frame vanish). Pi auto-detects the
|
|
43
|
+
// terminal background and picks a light or dark theme; ctx.ui.setHeader hands our factory
|
|
44
|
+
// that live Theme (and ctx.ui.theme exposes it to the sign-in widget), so the banner's
|
|
45
|
+
// colours resolve to dark ink on a light bg and light ink on a dark bg.
|
|
53
46
|
|
|
54
47
|
// The Privateer mark: our symbol — a padlock (with a keyhole) fused into an anchor,
|
|
55
48
|
// "bring your own model" meets lock-and-key privacy — drawn from the app's logo. It's
|
|
56
|
-
// rendered with terminal HALF-BLOCKS, so each text row packs TWO pixel rows
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
49
|
+
// rendered with terminal HALF-BLOCKS, so each text row packs TWO pixel rows. The mark is
|
|
50
|
+
// a SINGLE colour (every set pixel is the accent), so a cell never needs two different
|
|
51
|
+
// colours: both pixels set → a full block "█", top only → "▀", bottom only → "▄", none →
|
|
52
|
+
// a space — all painted with the accent as FOREGROUND, no background cells at all. That
|
|
53
|
+
// makes the mark inherit the theme's ink (dark on light, light on dark) with one colour,
|
|
54
|
+
// and sidesteps the old bg-bleed hazard entirely. Every built line is MARK_W visible
|
|
55
|
+
// cells wide (SGR escapes don't count), so the text column beside it stays aligned. To
|
|
56
|
+
// redraw: edit PIXELS (each char is "O" ink or "." transparent), keeping every row MARK_W
|
|
57
|
+
// long and the row COUNT even — the builder pairs rows into half-block cells.
|
|
63
58
|
const MARK_W = 12;
|
|
64
|
-
const PX: Record<string, number | null> = {
|
|
65
|
-
".": null, // transparent — the frame (and the knocked-out keyhole) shows through
|
|
66
|
-
O: 231, // white (#ffffff, top of the 256-color cube) — the silhouette, a clean single-color mark
|
|
67
|
-
};
|
|
68
59
|
// 12 wide; an EVEN number of rows so they pair cleanly into half-block cells. Two blank
|
|
69
60
|
// leading rows give the lock a little headroom without dropping the whole mark too low.
|
|
70
61
|
// A small padlock rides on top as the anchor's ring: a narrow rounded shackle over an
|
|
@@ -83,26 +74,27 @@ const PIXELS = [
|
|
|
83
74
|
".OO..OO..OO.", "..OO.OO.OO..",
|
|
84
75
|
"..OOOOOOOO..", "...OOOOOO...", "....OOOO....", ".....OO.....",
|
|
85
76
|
];
|
|
86
|
-
// Build the mark
|
|
87
|
-
//
|
|
88
|
-
|
|
77
|
+
// Build the mark for a given palette (the accent is the ink). Cheap — called once per
|
|
78
|
+
// header factory invocation, i.e. once per theme, not per frame. Each cell resets SGR so
|
|
79
|
+
// the accent can never bleed into the row padding the framer adds after it.
|
|
80
|
+
function buildMark(p: Palette): string[] {
|
|
89
81
|
const rows: string[] = [];
|
|
90
82
|
for (let r = 0; r < PIXELS.length; r += 2) {
|
|
91
83
|
const top = PIXELS[r];
|
|
92
84
|
const bot = PIXELS[r + 1] ?? ".".repeat(MARK_W);
|
|
93
85
|
let line = "";
|
|
94
86
|
for (let x = 0; x < MARK_W; x++) {
|
|
95
|
-
const t =
|
|
96
|
-
const
|
|
97
|
-
if (t
|
|
98
|
-
else if (t
|
|
99
|
-
else if (
|
|
100
|
-
else line +=
|
|
87
|
+
const t = top[x] === "O";
|
|
88
|
+
const b = bot[x] === "O";
|
|
89
|
+
if (t && b) line += `${p.ACCENT}█${p.RESET}`;
|
|
90
|
+
else if (t) line += `${p.ACCENT}▀${p.RESET}`;
|
|
91
|
+
else if (b) line += `${p.ACCENT}▄${p.RESET}`;
|
|
92
|
+
else line += " ";
|
|
101
93
|
}
|
|
102
94
|
rows.push(line);
|
|
103
95
|
}
|
|
104
96
|
return rows;
|
|
105
|
-
}
|
|
97
|
+
}
|
|
106
98
|
|
|
107
99
|
// Visible width = characters after stripping SGR escapes. Everything we render inside
|
|
108
100
|
// the box is ASCII or a BMP width-1 symbol, so a plain length is exact here.
|
|
@@ -140,16 +132,16 @@ function shortCwd(): string {
|
|
|
140
132
|
// - signed out AND the current model bills to a Privateer account → it can't run
|
|
141
133
|
// until they sign in, so say so plainly (warning)
|
|
142
134
|
// - signed out on their own key → a quiet tease that /login adds more
|
|
143
|
-
function accountLine(modelProvider?: string): string {
|
|
135
|
+
function accountLine(p: Palette, modelProvider?: string): string {
|
|
144
136
|
const u = priv.currentUser();
|
|
145
137
|
if (u) {
|
|
146
138
|
const label = clean(u.email ?? (u.solanaPublicKey ? u.solanaPublicKey.slice(0, 6) + "…" : u.id));
|
|
147
|
-
return `${GREEN}connected${DIM} as ${RESET}${
|
|
139
|
+
return `${p.GREEN}connected${p.DIM} as ${p.RESET}${p.INK}${label}${p.RESET}`;
|
|
148
140
|
}
|
|
149
141
|
if (modelProvider === "privateer") {
|
|
150
|
-
return `${YELLOW}not signed in · /login to use this model${RESET}`;
|
|
142
|
+
return `${p.YELLOW}not signed in · /login to use this model${p.RESET}`;
|
|
151
143
|
}
|
|
152
|
-
return `${DIM}not signed in · ${
|
|
144
|
+
return `${p.DIM}not signed in · ${p.INK}/login${p.DIM} to connect your account${p.RESET}`;
|
|
153
145
|
}
|
|
154
146
|
|
|
155
147
|
// Is dotted version `a` newer than `b`? Plain numeric compare of major.minor.patch —
|
|
@@ -167,12 +159,12 @@ function isNewer(a: string, b: string): boolean {
|
|
|
167
159
|
// The "update available" banner line, or "" when we're current / offline / unchecked.
|
|
168
160
|
// Reads the cache the launcher refreshes in the background (see bin/privateer-tui) —
|
|
169
161
|
// never fetches here, so the banner stays synchronous and never blocks on the network.
|
|
170
|
-
function updateNotice(): string {
|
|
162
|
+
function updateNotice(p: Palette): string {
|
|
171
163
|
try {
|
|
172
164
|
const home = process.env.PRIVATEER_HOME || join(homedir(), ".privateer");
|
|
173
165
|
const { latest } = JSON.parse(readFileSync(join(home, "update-check.json"), "utf8"));
|
|
174
166
|
if (typeof latest === "string" && isNewer(latest, VERSION)) {
|
|
175
|
-
return `${YELLOW}↑ v${latest} available${DIM} · run ${RESET}${
|
|
167
|
+
return `${p.YELLOW}↑ v${latest} available${p.DIM} · run ${p.RESET}${p.INK}privateer update${p.RESET}`;
|
|
176
168
|
}
|
|
177
169
|
} catch {
|
|
178
170
|
// no cache yet, unreadable, or malformed — show nothing.
|
|
@@ -184,16 +176,16 @@ function updateNotice(): string {
|
|
|
184
176
|
// loaded (so the moat's "the agent knows this project" state is visible), otherwise a
|
|
185
177
|
// quiet tease that /init scaffolds one. Reads the filesystem at render time, so it
|
|
186
178
|
// reflects the current cwd and updates after /init (via onContextChanged → refresh).
|
|
187
|
-
function contextLine(): string {
|
|
179
|
+
function contextLine(p: Palette): string {
|
|
188
180
|
const files = discoverContextFiles();
|
|
189
181
|
if (files.length === 0) {
|
|
190
|
-
return `${DIM}no PRIVATEER.md · ${
|
|
182
|
+
return `${p.DIM}no PRIVATEER.md · ${p.INK}/init${p.DIM} to add project context${p.RESET}`;
|
|
191
183
|
}
|
|
192
184
|
// Show the nearest (deepest, wins-last) file's path; note any additional ancestors
|
|
193
185
|
// with a "+N" so the header stays one line but the count isn't hidden.
|
|
194
186
|
const nearest = shortPath(files[files.length - 1].path);
|
|
195
|
-
const more = files.length > 1 ? `${DIM} +${files.length - 1}${RESET}` : "";
|
|
196
|
-
return `${GREEN}⚓${DIM} ${RESET}${
|
|
187
|
+
const more = files.length > 1 ? `${p.DIM} +${files.length - 1}${p.RESET}` : "";
|
|
188
|
+
return `${p.GREEN}⚓${p.DIM} ${p.RESET}${p.INK}${nearest}${p.RESET}${more}`;
|
|
197
189
|
}
|
|
198
190
|
|
|
199
191
|
// ── "What's New" — a tiny in-banner changelog ────────────────────────────────
|
|
@@ -206,11 +198,11 @@ const WHATS_NEW: Array<{ text: string; cmd?: string }> = [
|
|
|
206
198
|
{ text: "Self-update built in —", cmd: "privateer update" },
|
|
207
199
|
];
|
|
208
200
|
|
|
209
|
-
function whatsNewRows(): string[] {
|
|
210
|
-
const head = `${BOLD}${
|
|
201
|
+
function whatsNewRows(p: Palette): string[] {
|
|
202
|
+
const head = `${p.BOLD}${p.INK}✦ What's new${p.RESET}`;
|
|
211
203
|
const items = WHATS_NEW.map(
|
|
212
204
|
({ text, cmd }) =>
|
|
213
|
-
`${
|
|
205
|
+
`${p.ACCENT}·${p.RESET} ${p.DIM}${text}${p.RESET}${cmd ? ` ${p.INK}${cmd}${p.RESET}` : ""}`,
|
|
214
206
|
);
|
|
215
207
|
return [head, ...items];
|
|
216
208
|
}
|
|
@@ -220,51 +212,55 @@ function whatsNewRows(): string[] {
|
|
|
220
212
|
// mark), so we zip by row index and pad the short side — every text-only row lands in
|
|
221
213
|
// the same column as the rows beside the mark. One place owns the left gutter, so
|
|
222
214
|
// spacing can't drift between the mark rows and the trailing rows.
|
|
223
|
-
function renderBanner(width: number, modelProvider?: string): string[] {
|
|
215
|
+
function renderBanner(width: number, p: Palette, mark: string[], modelProvider?: string): string[] {
|
|
224
216
|
// Right column, top to bottom. The two leading blanks drop the wordmark down so it
|
|
225
217
|
// sits beside the lock body (not the shackle); the rest follows in reading order.
|
|
226
218
|
const text: string[] = [
|
|
227
219
|
"",
|
|
228
|
-
`${BOLD}${
|
|
229
|
-
`${DIM}Chart your own course privately.${RESET}`,
|
|
220
|
+
`${p.BOLD}${p.ACCENT}✻ ${p.ACCENT}P${p.INK}RIVATEER${p.RESET}${p.DIM} privateer-agent ${p.INK}v${VERSION}${p.RESET}`,
|
|
221
|
+
`${p.DIM}Chart your own course privately.${p.RESET}`,
|
|
230
222
|
"",
|
|
231
|
-
accountLine(modelProvider),
|
|
232
|
-
`${
|
|
233
|
-
contextLine(),
|
|
223
|
+
accountLine(p, modelProvider),
|
|
224
|
+
`${p.INK}${shortCwd()}${p.RESET}`,
|
|
225
|
+
contextLine(p),
|
|
234
226
|
];
|
|
235
|
-
const notice = updateNotice();
|
|
227
|
+
const notice = updateNotice(p);
|
|
236
228
|
if (notice) text.push(notice);
|
|
237
229
|
// A blank spacer, then the What's New block — set off below the identity lines.
|
|
238
|
-
text.push("", ...whatsNewRows());
|
|
230
|
+
text.push("", ...whatsNewRows(p));
|
|
239
231
|
|
|
240
232
|
// Zip the mark and the text column by row. Rows past the mark's height get a blank
|
|
241
233
|
// gutter of the mark's width, so the text stays in one column throughout.
|
|
242
234
|
const gap = " ";
|
|
243
|
-
const height = Math.max(
|
|
235
|
+
const height = Math.max(mark.length, text.length);
|
|
244
236
|
const rows: string[] = [];
|
|
245
237
|
for (let i = 0; i < height; i++) {
|
|
246
238
|
// The mark lines already carry their own per-pixel colors, so we don't wrap them.
|
|
247
|
-
const left = i <
|
|
239
|
+
const left = i < mark.length ? mark[i] : " ".repeat(MARK_W);
|
|
248
240
|
rows.push(`${left}${gap}${text[i] ?? ""}`.trimEnd());
|
|
249
241
|
}
|
|
250
242
|
|
|
251
243
|
const cap = Math.max(20, width - 4); // 2 border cells + 2 padding
|
|
252
244
|
const inner = Math.min(cap, Math.max(...rows.map(vlen)));
|
|
253
245
|
const bar = "─".repeat(inner + 2);
|
|
254
|
-
const out = [`${BORDER}╭${bar}╮${RESET}`];
|
|
246
|
+
const out = [`${p.BORDER}╭${bar}╮${p.RESET}`];
|
|
255
247
|
for (const row of rows) {
|
|
256
248
|
const pad = Math.max(0, inner - vlen(row));
|
|
257
|
-
out.push(`${BORDER}│${RESET} ${row}${" ".repeat(pad)} ${BORDER}│${RESET}`);
|
|
249
|
+
out.push(`${p.BORDER}│${p.RESET} ${row}${" ".repeat(pad)} ${p.BORDER}│${p.RESET}`);
|
|
258
250
|
}
|
|
259
|
-
out.push(`${BORDER}╰${bar}╯${RESET}`);
|
|
251
|
+
out.push(`${p.BORDER}╰${bar}╯${p.RESET}`);
|
|
260
252
|
return out;
|
|
261
253
|
}
|
|
262
254
|
|
|
263
255
|
// A Pi header Component (setHeader factory return). Static banner; captures the model
|
|
264
|
-
// provider so the account line reflects the picked model
|
|
265
|
-
|
|
256
|
+
// provider so the account line reflects the picked model, and the live theme so every
|
|
257
|
+
// colour tracks the terminal background (dark ink on light, light ink on dark). The
|
|
258
|
+
// palette and mark are resolved once here (per theme), not per frame.
|
|
259
|
+
function headerComponent(theme: any, modelProvider?: string) {
|
|
260
|
+
const p = paletteFor(theme);
|
|
261
|
+
const mark = buildMark(p);
|
|
266
262
|
return {
|
|
267
|
-
render: (width: number): string[] => renderBanner(width, modelProvider),
|
|
263
|
+
render: (width: number): string[] => renderBanner(width, p, mark, modelProvider),
|
|
268
264
|
invalidate() {},
|
|
269
265
|
};
|
|
270
266
|
}
|
|
@@ -293,8 +289,12 @@ export default function privateerBrand(pi: any): void {
|
|
|
293
289
|
}
|
|
294
290
|
};
|
|
295
291
|
|
|
292
|
+
// Pi calls the factory with (tui, theme) — pass the live theme through so the banner
|
|
293
|
+
// paints from it. Falls back to ctx.ui.theme when a Pi build hands the factory no theme.
|
|
296
294
|
const setHeader = (ctx: any) =>
|
|
297
|
-
ctx?.ui?.setHeader?.(() =>
|
|
295
|
+
ctx?.ui?.setHeader?.((_tui: any, theme: any) =>
|
|
296
|
+
headerComponent(theme ?? ctx?.ui?.theme, currentModelProvider),
|
|
297
|
+
);
|
|
298
298
|
|
|
299
299
|
const refresh = (ctx: any) => {
|
|
300
300
|
dbg(`refresh: hasUI=${!!ctx?.hasUI} hasSetHeader=${typeof ctx?.ui?.setHeader} user=${priv.currentUser()?.email ?? null}`);
|
|
@@ -355,6 +355,7 @@ export default function privateerBrand(pi: any): void {
|
|
|
355
355
|
}
|
|
356
356
|
ctx?.ui?.notify?.("Connecting to Privateer — requesting a device code…", "info");
|
|
357
357
|
try {
|
|
358
|
+
const p = paletteFor(ctx?.ui?.theme);
|
|
358
359
|
const user = await priv.runDeviceLogin({
|
|
359
360
|
onCode: (code: any) => {
|
|
360
361
|
const uri = clean(code.verification_uri_complete ?? code.verification_uri ?? "");
|
|
@@ -362,11 +363,11 @@ export default function privateerBrand(pi: any): void {
|
|
|
362
363
|
ctx?.ui?.setWidget?.(
|
|
363
364
|
"privateer-signin",
|
|
364
365
|
[
|
|
365
|
-
`${
|
|
366
|
-
`${DIM}Approve this terminal in the Privateer app:${RESET}`,
|
|
367
|
-
` code ${BOLD}${
|
|
368
|
-
uri ? `${DIM} or open ${RESET}${
|
|
369
|
-
`${DIM} waiting for approval…${RESET}`,
|
|
366
|
+
`${p.INK}⚓ Sign in to Privateer${p.RESET}`,
|
|
367
|
+
`${p.DIM}Approve this terminal in the Privateer app:${p.RESET}`,
|
|
368
|
+
` code ${p.BOLD}${p.ACCENT}${userCode}${p.RESET}`,
|
|
369
|
+
uri ? `${p.DIM} or open ${p.RESET}${p.INK}${uri}${p.RESET}` : "",
|
|
370
|
+
`${p.DIM} waiting for approval…${p.RESET}`,
|
|
370
371
|
].filter(Boolean),
|
|
371
372
|
{ placement: "aboveEditor" },
|
|
372
373
|
);
|
|
@@ -406,6 +407,47 @@ export default function privateerBrand(pi: any): void {
|
|
|
406
407
|
);
|
|
407
408
|
}
|
|
408
409
|
|
|
410
|
+
// Move the LIVE session onto a confidential model the instant the user signs in. A
|
|
411
|
+
// terminal launched with no credentials is pinned by `--model` to the keyless
|
|
412
|
+
// OpenRouter fallback; without this switch it stays there and the first prompt after
|
|
413
|
+
// sign-in dead-ends on "No API key found for openrouter". resolveSignedInModel picks
|
|
414
|
+
// Tinfoil GLM 5.2 (client-attested TEE) when a key is present, else the account's NEAR
|
|
415
|
+
// channel — private inference that works out of the box. We only override an auto-picked
|
|
416
|
+
// launch model, never a deliberate PRIVATEER_MODEL, and never re-switch if we're already
|
|
417
|
+
// on the target. The account (NEAR) credential is spawned moments AFTER sign-in fires,
|
|
418
|
+
// so setModel can briefly return false ("no key yet"); retry a few times so the switch
|
|
419
|
+
// lands as soon as the credential is ready (Tinfoil, key already in env, succeeds first
|
|
420
|
+
// try). Best-effort throughout — a failure just leaves the launch model in place.
|
|
421
|
+
async function activateSignedInModel(ctx: any): Promise<void> {
|
|
422
|
+
if (process.env.PRIVATEER_MODEL?.trim()) return; // deliberate override — respect it
|
|
423
|
+
const reg = ctx?.modelRegistry;
|
|
424
|
+
if (!reg?.find || typeof pi.setModel !== "function") return;
|
|
425
|
+
const spec = resolveSignedInModel();
|
|
426
|
+
const slash = spec.indexOf("/");
|
|
427
|
+
if (slash <= 0) return;
|
|
428
|
+
const provider = spec.slice(0, slash), id = spec.slice(slash + 1);
|
|
429
|
+
const currentSpec = ctx?.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
|
|
430
|
+
if (currentSpec === spec) return; // already there — nothing to do
|
|
431
|
+
const model = reg.find(provider, id);
|
|
432
|
+
if (!model) { dbg(`activateSignedInModel: ${spec} not in registry`); return; }
|
|
433
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
434
|
+
try {
|
|
435
|
+
const ok = await pi.setModel(model);
|
|
436
|
+
if (ok !== false) {
|
|
437
|
+
currentModelProvider = provider;
|
|
438
|
+
refresh(ctx);
|
|
439
|
+
ctx?.ui?.notify?.(`Now using ${spec} for private inference.`, "info");
|
|
440
|
+
dbg(`activateSignedInModel: switched to ${spec}`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
} catch (e) {
|
|
444
|
+
dbg(`activateSignedInModel: setModel threw ${(e as Error).message}`);
|
|
445
|
+
}
|
|
446
|
+
await new Promise((r) => setTimeout(r, 400)); // credential still spawning — retry
|
|
447
|
+
}
|
|
448
|
+
dbg(`activateSignedInModel: gave up switching to ${spec}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
409
451
|
dbg("extension loaded, onSignedIn listener registering");
|
|
410
452
|
|
|
411
453
|
pi.on("session_start", (_e: any, ctx: any) => {
|
|
@@ -451,6 +493,9 @@ export default function privateerBrand(pi: any): void {
|
|
|
451
493
|
priv.onSignedIn(() => {
|
|
452
494
|
dbg(`onSignedIn fired; ctxRef=${ctxRef ? "set" : "null"}`);
|
|
453
495
|
refresh(ctxRef);
|
|
496
|
+
// Activate a confidential model in the live session so the user can prompt right
|
|
497
|
+
// away instead of dead-ending on the keyless launch model. See activateSignedInModel.
|
|
498
|
+
void activateSignedInModel(ctxRef);
|
|
454
499
|
});
|
|
455
500
|
|
|
456
501
|
// /init (in privateer-context) just created or changed a PRIVATEER.md — re-render the
|
|
@@ -29,6 +29,7 @@ import { agentDir } from "../src/config/paths.ts";
|
|
|
29
29
|
import { agentVersion } from "../src/config/version.ts";
|
|
30
30
|
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import * as priv from "../src/auth/privateer.ts";
|
|
32
|
+
import { paletteFor } from "../src/ui/palette.ts";
|
|
32
33
|
import type { PermissionMode } from "../src/config/permissionMode.ts";
|
|
33
34
|
|
|
34
35
|
const MODES: PermissionMode[] = ["default", "acceptEdits", "bypass", "plan"];
|
|
@@ -221,7 +222,6 @@ function advertiseCommands(): { name: string; description?: string }[] {
|
|
|
221
222
|
// driven from the phone — with a reminder that `/remote-access off` stops it. We
|
|
222
223
|
// keep a UI handle (captured from session_start / the command ctx) so the relay's
|
|
223
224
|
// own connect/disconnect callbacks can refresh the indicator, not just the command.
|
|
224
|
-
const GREEN = "\x1b[32m", YELLOW = "\x1b[33m", DIM = "\x1b[2m", RESET = "\x1b[0m";
|
|
225
225
|
const REMOTE_STATUS_KEY = "privateer:remote-access";
|
|
226
226
|
let uiRef: any = null;
|
|
227
227
|
// "off" → no indicator; "connecting" → relay starting or reconnecting (yellow);
|
|
@@ -235,10 +235,13 @@ function refreshRemoteStatus(): void {
|
|
|
235
235
|
ui.setStatus(REMOTE_STATUS_KEY, undefined);
|
|
236
236
|
return;
|
|
237
237
|
}
|
|
238
|
+
// Paint from the active theme so the footer reads on a light terminal too (a bare
|
|
239
|
+
// green/yellow escape can wash out on white) — falls back to white on no theme.
|
|
240
|
+
const p = paletteFor(ui.theme);
|
|
238
241
|
const text =
|
|
239
242
|
remoteState === "connected"
|
|
240
|
-
? `${GREEN}⟿ remote access${RESET} ${DIM}· /remote-access off to stop${RESET}`
|
|
241
|
-
: `${YELLOW}⟿ remote access · connecting…${RESET} ${DIM}· /remote-access off to stop${RESET}`;
|
|
243
|
+
? `${p.GREEN}⟿ remote access${p.RESET} ${p.DIM}· /remote-access off to stop${p.RESET}`
|
|
244
|
+
: `${p.YELLOW}⟿ remote access · connecting…${p.RESET} ${p.DIM}· /remote-access off to stop${p.RESET}`;
|
|
242
245
|
ui.setStatus(REMOTE_STATUS_KEY, text);
|
|
243
246
|
}
|
|
244
247
|
|