@jameslovespancakes/pi-plus 1.0.0 → 1.0.1
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/LICENSE +21 -21
- package/README.md +190 -190
- package/config/pi-plus.example.json +60 -60
- package/config/skills/model-routing/SKILL.md +86 -86
- package/images/pi-plus.svg +10 -10
- package/package.json +67 -67
- package/server/board-server.mjs +641 -641
- package/server/package.json +17 -17
- package/src/core/accounts/registry.ts +93 -93
- package/src/core/anthropic/client-identity.ts +241 -241
- package/src/core/catalog/quality.ts +314 -314
- package/src/core/config.ts +169 -169
- package/src/core/env.ts +58 -58
- package/src/core/exec/process.ts +146 -146
- package/src/core/exec/ssh-config.ts +157 -157
- package/src/core/policy/policy.ts +183 -183
- package/src/core/quota/pool.ts +64 -64
- package/src/core/quota/usage-source.ts +289 -289
- package/src/core/store.ts +43 -43
- package/src/domains/agents/board-setup.ts +409 -409
- package/src/domains/agents/index.ts +462 -462
- package/src/domains/models/catalog-tool.ts +361 -361
- package/src/domains/models/index.ts +14 -14
- package/src/domains/models/policy-gate.ts +169 -169
- package/src/domains/models/provider-picker.ts +207 -207
- package/src/domains/remote/config-path.ts +41 -41
- package/src/domains/remote/index.ts +866 -866
- package/src/domains/remote/setup.ts +425 -425
- package/src/domains/setup/index.ts +220 -220
- package/src/domains/subscriptions/accounts.ts +242 -242
- package/src/domains/subscriptions/footer.ts +182 -182
- package/src/domains/subscriptions/index.ts +42 -42
- package/src/domains/subscriptions/provider.ts +219 -219
- package/src/domains/subscriptions/providers/anthropic.ts +149 -149
- package/src/domains/subscriptions/providers/codex.ts +148 -148
- package/src/domains/subscriptions/routing.ts +72 -72
- package/src/services/usage-service.ts +186 -186
- package/src/ui/format.ts +73 -73
- package/src/ui/usage-bars.ts +154 -154
- package/src/vendor/anthropic.ts +109 -109
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
-
import { hasTruecolor, levelColor } from "../../ui/format.ts";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The `/provider` picker.
|
|
6
|
-
*
|
|
7
|
-
* Built on SettingsList so toggling happens *in place*: the component persists
|
|
8
|
-
* and only its value strings change, rather than the dialog closing and
|
|
9
|
-
* reopening on every keypress, which caused a full-screen redraw (the flicker).
|
|
10
|
-
*
|
|
11
|
-
* States are colour coded rather than prefixed, so the list reads at a glance:
|
|
12
|
-
*
|
|
13
|
-
* Allowed green reachable without a prompt
|
|
14
|
-
* Approved cyan metered, granted for this session
|
|
15
|
-
* Needs Approval yellow metered, will prompt
|
|
16
|
-
* Denied red blocked by policy outright
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export const STATE_TEXT = {
|
|
20
|
-
auto: "Allowed",
|
|
21
|
-
approved: "Approved",
|
|
22
|
-
blocked: "Needs Approval",
|
|
23
|
-
denied: "Denied",
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
|
-
export type StateKey = keyof typeof STATE_TEXT;
|
|
27
|
-
|
|
28
|
-
/** The two values SettingsList cycles between for a binary toggle. */
|
|
29
|
-
export const TOGGLE_VALUES = [STATE_TEXT.auto, STATE_TEXT.blocked];
|
|
30
|
-
|
|
31
|
-
export interface ProviderRow {
|
|
32
|
-
/** Stable id used by SettingsList and by the toggle handler. */
|
|
33
|
-
id: string;
|
|
34
|
-
/** Provider id this row controls. */
|
|
35
|
-
provider: string;
|
|
36
|
-
/** Human name, e.g. "Anthropic" or "OpenRouter". */
|
|
37
|
-
display: string;
|
|
38
|
-
state: StateKey;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Where each state sits on the usage-bar gradient.
|
|
43
|
-
*
|
|
44
|
-
* `levelColor` maps 0-100 onto the same red-to-green ramp the quota bars use,
|
|
45
|
-
* so "Allowed" is the green of a full bar and "Needs Approval" is the amber of
|
|
46
|
-
* one running low. Reusing the ramp keeps the two surfaces consistent instead
|
|
47
|
-
* of pairing a bespoke green here with a different green in the footer.
|
|
48
|
-
*/
|
|
49
|
-
const STATE_LEVEL: Record<StateKey, number> = {
|
|
50
|
-
auto: 100,
|
|
51
|
-
approved: 100,
|
|
52
|
-
blocked: 45,
|
|
53
|
-
denied: 0,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/** Fallback for terminals without truecolor. */
|
|
57
|
-
const STATE_THEME_COLOUR: Record<StateKey, string> = {
|
|
58
|
-
auto: "success",
|
|
59
|
-
approved: "success",
|
|
60
|
-
blocked: "warning",
|
|
61
|
-
denied: "error",
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Colour a state string. Kept separate from rendering so callers can reuse it
|
|
66
|
-
* for notices and be consistent with the list.
|
|
67
|
-
*/
|
|
68
|
-
export function colourState(theme: any, state: StateKey, text: string = STATE_TEXT[state]): string {
|
|
69
|
-
// Approved shares auto's green: both mean usable, and a third hue would imply
|
|
70
|
-
// a third risk level. The label already distinguishes them.
|
|
71
|
-
if (hasTruecolor()) return levelColor(STATE_LEVEL[state])(text);
|
|
72
|
-
return theme.fg(STATE_THEME_COLOUR[state], text);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** The row label: a status dot on the usage-bar ramp, plus the name. */
|
|
76
|
-
function labelFor(theme: any, row: ProviderRow): string {
|
|
77
|
-
const dot = colourState(theme, row.state, "●");
|
|
78
|
-
const name = row.state === "denied" ? theme.fg("muted", row.display) : row.display;
|
|
79
|
-
return `${dot} ${name}`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The chrome pi uses for `/model` and its other in-chat pickers.
|
|
84
|
-
*
|
|
85
|
-
* Not a box: a full-width accent rule, a bold title, the body, then a closing
|
|
86
|
-
* rule. Reproduced from pi's own `frame(theme, title, body, footer)` helper so
|
|
87
|
-
* this reads as part of the chat flow rather than as a floating dialog.
|
|
88
|
-
*
|
|
89
|
-
* Input and mouse events pass straight through, so the frame is presentation
|
|
90
|
-
* only and does not disturb the in-place updates.
|
|
91
|
-
*/
|
|
92
|
-
function framed(theme: any, list: any, title: string): Component {
|
|
93
|
-
const rule = (width: number) => theme.fg("accent", "─".repeat(Math.max(1, width)));
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
invalidate: () => list.invalidate?.(),
|
|
97
|
-
handleInput: (data: string) => list.handleInput(data),
|
|
98
|
-
handleMouse: (event: any) => list.handleMouse?.(event),
|
|
99
|
-
render(width: number): string[] {
|
|
100
|
-
const inner = Math.max(1, width);
|
|
101
|
-
// pi pads title and footer by one column; the list renders flush.
|
|
102
|
-
return [
|
|
103
|
-
rule(inner),
|
|
104
|
-
` ${theme.fg("accent", theme.bold(title))}`,
|
|
105
|
-
...list.render(inner),
|
|
106
|
-
rule(inner),
|
|
107
|
-
];
|
|
108
|
-
},
|
|
109
|
-
} as Component;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Theme for the picker. Every callback takes `selected` so the highlighted row
|
|
114
|
-
* can be emphasised without the caller tracking cursor position.
|
|
115
|
-
*/
|
|
116
|
-
function pickerTheme(theme: any) {
|
|
117
|
-
return {
|
|
118
|
-
label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text),
|
|
119
|
-
value: (text: string, _selected: boolean) => text,
|
|
120
|
-
description: (text: string) => theme.fg("dim", text),
|
|
121
|
-
cursor: theme.fg("accent", "›"),
|
|
122
|
-
hint: (text: string) => theme.fg("dim", text),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export interface PickerDeps {
|
|
127
|
-
/** Re-reads the current rows, so the picker never shows stale state. */
|
|
128
|
-
rows: () => Promise<ProviderRow[]>;
|
|
129
|
-
/**
|
|
130
|
-
* Applies a toggle and returns the resulting state.
|
|
131
|
-
*
|
|
132
|
-
* Deliberately synchronous. SettingsList cycles its own `values` the instant
|
|
133
|
-
* Enter is pressed, so an async toggle would let the text change one frame
|
|
134
|
-
* before the colour caught up. Keeping it sync means the label, the dot and
|
|
135
|
-
* the value all land in the same render.
|
|
136
|
-
*/
|
|
137
|
-
toggle: (provider: string) => StateKey;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Opens the picker and resolves when the user cancels.
|
|
142
|
-
*
|
|
143
|
-
* The loop only re-enters when the row set itself changes (for example a
|
|
144
|
-
* provider appearing after re-authentication); ordinary toggles just call
|
|
145
|
-
* `updateValue`, which is a single re-render of one component.
|
|
146
|
-
*/
|
|
147
|
-
export async function openProviderPicker(ctx: any, deps: PickerDeps): Promise<void> {
|
|
148
|
-
const { SettingsList } = await import("@earendil-works/pi-tui");
|
|
149
|
-
|
|
150
|
-
const initial = await deps.rows();
|
|
151
|
-
if (initial.length === 0) {
|
|
152
|
-
ctx.ui.notify("No providers are configured. Sign in with /account or pi auth.", "info");
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
await ctx.ui.custom((_tui: any, theme: any, _keys: any, done: (v: void) => void) => {
|
|
157
|
-
const rows = initial;
|
|
158
|
-
let list: any;
|
|
159
|
-
|
|
160
|
-
// Held by reference: SettingsList reads these objects on every render, so
|
|
161
|
-
// mutating `label` here recolours the status dot without rebuilding the
|
|
162
|
-
// component. `updateValue` only refreshes the value column.
|
|
163
|
-
// `values` carries the COLOURED strings, not plain text. SettingsList shows
|
|
164
|
-
// whichever it cycles to immediately, so pre-colouring them means the new
|
|
165
|
-
// text arrives already in the right colour rather than flashing uncoloured.
|
|
166
|
-
const colouredToggle = [colourState(theme, "auto"), colourState(theme, "blocked")];
|
|
167
|
-
|
|
168
|
-
const items = rows.map((row) => ({
|
|
169
|
-
id: row.id,
|
|
170
|
-
label: labelFor(theme, row),
|
|
171
|
-
values: [...colouredToggle],
|
|
172
|
-
currentValue: colourState(theme, row.state),
|
|
173
|
-
}));
|
|
174
|
-
|
|
175
|
-
// Synchronous throughout: label, dot and value all change in one render.
|
|
176
|
-
const onChange = (id: string) => {
|
|
177
|
-
const row = rows.find((r) => r.id === id);
|
|
178
|
-
const item = items.find((i) => i.id === id);
|
|
179
|
-
if (!row || !item) return;
|
|
180
|
-
|
|
181
|
-
if (row.state === "denied") {
|
|
182
|
-
// Undo the value SettingsList optimistically cycled to.
|
|
183
|
-
list?.updateValue(id, colourState(theme, row.state));
|
|
184
|
-
ctx.ui.notify(`${row.display} is denied in policy. Edit pi-plus.json to change that.`, "warning");
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const next = deps.toggle(row.provider);
|
|
189
|
-
row.state = next;
|
|
190
|
-
item.label = labelFor(theme, row);
|
|
191
|
-
list?.updateValue(id, colourState(theme, next));
|
|
192
|
-
list?.invalidate?.();
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
list = new SettingsList(
|
|
196
|
-
items,
|
|
197
|
-
12,
|
|
198
|
-
pickerTheme(theme),
|
|
199
|
-
onChange,
|
|
200
|
-
() => done(undefined),
|
|
201
|
-
{ enableSearch: false },
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
return framed(theme, list, "Providers") as Component & { dispose?(): void };
|
|
205
|
-
});
|
|
206
|
-
// No `overlay` option: the picker renders inline in the chat flow rather than
|
|
207
|
-
// floating over it.
|
|
1
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import { hasTruecolor, levelColor } from "../../ui/format.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `/provider` picker.
|
|
6
|
+
*
|
|
7
|
+
* Built on SettingsList so toggling happens *in place*: the component persists
|
|
8
|
+
* and only its value strings change, rather than the dialog closing and
|
|
9
|
+
* reopening on every keypress, which caused a full-screen redraw (the flicker).
|
|
10
|
+
*
|
|
11
|
+
* States are colour coded rather than prefixed, so the list reads at a glance:
|
|
12
|
+
*
|
|
13
|
+
* Allowed green reachable without a prompt
|
|
14
|
+
* Approved cyan metered, granted for this session
|
|
15
|
+
* Needs Approval yellow metered, will prompt
|
|
16
|
+
* Denied red blocked by policy outright
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const STATE_TEXT = {
|
|
20
|
+
auto: "Allowed",
|
|
21
|
+
approved: "Approved",
|
|
22
|
+
blocked: "Needs Approval",
|
|
23
|
+
denied: "Denied",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
export type StateKey = keyof typeof STATE_TEXT;
|
|
27
|
+
|
|
28
|
+
/** The two values SettingsList cycles between for a binary toggle. */
|
|
29
|
+
export const TOGGLE_VALUES = [STATE_TEXT.auto, STATE_TEXT.blocked];
|
|
30
|
+
|
|
31
|
+
export interface ProviderRow {
|
|
32
|
+
/** Stable id used by SettingsList and by the toggle handler. */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Provider id this row controls. */
|
|
35
|
+
provider: string;
|
|
36
|
+
/** Human name, e.g. "Anthropic" or "OpenRouter". */
|
|
37
|
+
display: string;
|
|
38
|
+
state: StateKey;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Where each state sits on the usage-bar gradient.
|
|
43
|
+
*
|
|
44
|
+
* `levelColor` maps 0-100 onto the same red-to-green ramp the quota bars use,
|
|
45
|
+
* so "Allowed" is the green of a full bar and "Needs Approval" is the amber of
|
|
46
|
+
* one running low. Reusing the ramp keeps the two surfaces consistent instead
|
|
47
|
+
* of pairing a bespoke green here with a different green in the footer.
|
|
48
|
+
*/
|
|
49
|
+
const STATE_LEVEL: Record<StateKey, number> = {
|
|
50
|
+
auto: 100,
|
|
51
|
+
approved: 100,
|
|
52
|
+
blocked: 45,
|
|
53
|
+
denied: 0,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Fallback for terminals without truecolor. */
|
|
57
|
+
const STATE_THEME_COLOUR: Record<StateKey, string> = {
|
|
58
|
+
auto: "success",
|
|
59
|
+
approved: "success",
|
|
60
|
+
blocked: "warning",
|
|
61
|
+
denied: "error",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Colour a state string. Kept separate from rendering so callers can reuse it
|
|
66
|
+
* for notices and be consistent with the list.
|
|
67
|
+
*/
|
|
68
|
+
export function colourState(theme: any, state: StateKey, text: string = STATE_TEXT[state]): string {
|
|
69
|
+
// Approved shares auto's green: both mean usable, and a third hue would imply
|
|
70
|
+
// a third risk level. The label already distinguishes them.
|
|
71
|
+
if (hasTruecolor()) return levelColor(STATE_LEVEL[state])(text);
|
|
72
|
+
return theme.fg(STATE_THEME_COLOUR[state], text);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The row label: a status dot on the usage-bar ramp, plus the name. */
|
|
76
|
+
function labelFor(theme: any, row: ProviderRow): string {
|
|
77
|
+
const dot = colourState(theme, row.state, "●");
|
|
78
|
+
const name = row.state === "denied" ? theme.fg("muted", row.display) : row.display;
|
|
79
|
+
return `${dot} ${name}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The chrome pi uses for `/model` and its other in-chat pickers.
|
|
84
|
+
*
|
|
85
|
+
* Not a box: a full-width accent rule, a bold title, the body, then a closing
|
|
86
|
+
* rule. Reproduced from pi's own `frame(theme, title, body, footer)` helper so
|
|
87
|
+
* this reads as part of the chat flow rather than as a floating dialog.
|
|
88
|
+
*
|
|
89
|
+
* Input and mouse events pass straight through, so the frame is presentation
|
|
90
|
+
* only and does not disturb the in-place updates.
|
|
91
|
+
*/
|
|
92
|
+
function framed(theme: any, list: any, title: string): Component {
|
|
93
|
+
const rule = (width: number) => theme.fg("accent", "─".repeat(Math.max(1, width)));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
invalidate: () => list.invalidate?.(),
|
|
97
|
+
handleInput: (data: string) => list.handleInput(data),
|
|
98
|
+
handleMouse: (event: any) => list.handleMouse?.(event),
|
|
99
|
+
render(width: number): string[] {
|
|
100
|
+
const inner = Math.max(1, width);
|
|
101
|
+
// pi pads title and footer by one column; the list renders flush.
|
|
102
|
+
return [
|
|
103
|
+
rule(inner),
|
|
104
|
+
` ${theme.fg("accent", theme.bold(title))}`,
|
|
105
|
+
...list.render(inner),
|
|
106
|
+
rule(inner),
|
|
107
|
+
];
|
|
108
|
+
},
|
|
109
|
+
} as Component;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Theme for the picker. Every callback takes `selected` so the highlighted row
|
|
114
|
+
* can be emphasised without the caller tracking cursor position.
|
|
115
|
+
*/
|
|
116
|
+
function pickerTheme(theme: any) {
|
|
117
|
+
return {
|
|
118
|
+
label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text),
|
|
119
|
+
value: (text: string, _selected: boolean) => text,
|
|
120
|
+
description: (text: string) => theme.fg("dim", text),
|
|
121
|
+
cursor: theme.fg("accent", "›"),
|
|
122
|
+
hint: (text: string) => theme.fg("dim", text),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface PickerDeps {
|
|
127
|
+
/** Re-reads the current rows, so the picker never shows stale state. */
|
|
128
|
+
rows: () => Promise<ProviderRow[]>;
|
|
129
|
+
/**
|
|
130
|
+
* Applies a toggle and returns the resulting state.
|
|
131
|
+
*
|
|
132
|
+
* Deliberately synchronous. SettingsList cycles its own `values` the instant
|
|
133
|
+
* Enter is pressed, so an async toggle would let the text change one frame
|
|
134
|
+
* before the colour caught up. Keeping it sync means the label, the dot and
|
|
135
|
+
* the value all land in the same render.
|
|
136
|
+
*/
|
|
137
|
+
toggle: (provider: string) => StateKey;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Opens the picker and resolves when the user cancels.
|
|
142
|
+
*
|
|
143
|
+
* The loop only re-enters when the row set itself changes (for example a
|
|
144
|
+
* provider appearing after re-authentication); ordinary toggles just call
|
|
145
|
+
* `updateValue`, which is a single re-render of one component.
|
|
146
|
+
*/
|
|
147
|
+
export async function openProviderPicker(ctx: any, deps: PickerDeps): Promise<void> {
|
|
148
|
+
const { SettingsList } = await import("@earendil-works/pi-tui");
|
|
149
|
+
|
|
150
|
+
const initial = await deps.rows();
|
|
151
|
+
if (initial.length === 0) {
|
|
152
|
+
ctx.ui.notify("No providers are configured. Sign in with /account or pi auth.", "info");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await ctx.ui.custom((_tui: any, theme: any, _keys: any, done: (v: void) => void) => {
|
|
157
|
+
const rows = initial;
|
|
158
|
+
let list: any;
|
|
159
|
+
|
|
160
|
+
// Held by reference: SettingsList reads these objects on every render, so
|
|
161
|
+
// mutating `label` here recolours the status dot without rebuilding the
|
|
162
|
+
// component. `updateValue` only refreshes the value column.
|
|
163
|
+
// `values` carries the COLOURED strings, not plain text. SettingsList shows
|
|
164
|
+
// whichever it cycles to immediately, so pre-colouring them means the new
|
|
165
|
+
// text arrives already in the right colour rather than flashing uncoloured.
|
|
166
|
+
const colouredToggle = [colourState(theme, "auto"), colourState(theme, "blocked")];
|
|
167
|
+
|
|
168
|
+
const items = rows.map((row) => ({
|
|
169
|
+
id: row.id,
|
|
170
|
+
label: labelFor(theme, row),
|
|
171
|
+
values: [...colouredToggle],
|
|
172
|
+
currentValue: colourState(theme, row.state),
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
// Synchronous throughout: label, dot and value all change in one render.
|
|
176
|
+
const onChange = (id: string) => {
|
|
177
|
+
const row = rows.find((r) => r.id === id);
|
|
178
|
+
const item = items.find((i) => i.id === id);
|
|
179
|
+
if (!row || !item) return;
|
|
180
|
+
|
|
181
|
+
if (row.state === "denied") {
|
|
182
|
+
// Undo the value SettingsList optimistically cycled to.
|
|
183
|
+
list?.updateValue(id, colourState(theme, row.state));
|
|
184
|
+
ctx.ui.notify(`${row.display} is denied in policy. Edit pi-plus.json to change that.`, "warning");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const next = deps.toggle(row.provider);
|
|
189
|
+
row.state = next;
|
|
190
|
+
item.label = labelFor(theme, row);
|
|
191
|
+
list?.updateValue(id, colourState(theme, next));
|
|
192
|
+
list?.invalidate?.();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
list = new SettingsList(
|
|
196
|
+
items,
|
|
197
|
+
12,
|
|
198
|
+
pickerTheme(theme),
|
|
199
|
+
onChange,
|
|
200
|
+
() => done(undefined),
|
|
201
|
+
{ enableSearch: false },
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return framed(theme, list, "Providers") as Component & { dispose?(): void };
|
|
205
|
+
});
|
|
206
|
+
// No `overlay` option: the picker renders inline in the chat flow rather than
|
|
207
|
+
// floating over it.
|
|
208
208
|
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { readConfig, updateConfig } from "../../core/config.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Accessors for the `remote` section of `pi-plus.json`.
|
|
5
|
-
*
|
|
6
|
-
* The worker list previously lived in its own `remote-workers.json`, then
|
|
7
|
-
* `remote.json`. Both are migrated by core/config.ts on first read.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface RemoteWorkerRecord {
|
|
11
|
-
name: string;
|
|
12
|
-
ssh: string;
|
|
13
|
-
root?: string;
|
|
14
|
-
nice?: number;
|
|
15
|
-
tags?: string[];
|
|
16
|
-
identityFile?: string;
|
|
17
|
-
port?: number;
|
|
18
|
-
enabled?: boolean;
|
|
19
|
-
[key: string]: unknown;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface RemoteSettings {
|
|
23
|
-
injectStatus: boolean;
|
|
24
|
-
defaults: Record<string, unknown>;
|
|
25
|
-
workers: RemoteWorkerRecord[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function readRemote(): RemoteSettings {
|
|
29
|
-
const section = readConfig().remote;
|
|
30
|
-
return {
|
|
31
|
-
injectStatus: section.injectStatus !== false,
|
|
32
|
-
defaults: (section.defaults as Record<string, unknown>) ?? {},
|
|
33
|
-
workers: (section.workers as RemoteWorkerRecord[]) ?? [],
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function writeRemoteWorkers(workers: RemoteWorkerRecord[]): boolean {
|
|
38
|
-
return updateConfig((config) => {
|
|
39
|
-
config.remote.workers = workers;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
1
|
+
import { readConfig, updateConfig } from "../../core/config.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Accessors for the `remote` section of `pi-plus.json`.
|
|
5
|
+
*
|
|
6
|
+
* The worker list previously lived in its own `remote-workers.json`, then
|
|
7
|
+
* `remote.json`. Both are migrated by core/config.ts on first read.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface RemoteWorkerRecord {
|
|
11
|
+
name: string;
|
|
12
|
+
ssh: string;
|
|
13
|
+
root?: string;
|
|
14
|
+
nice?: number;
|
|
15
|
+
tags?: string[];
|
|
16
|
+
identityFile?: string;
|
|
17
|
+
port?: number;
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RemoteSettings {
|
|
23
|
+
injectStatus: boolean;
|
|
24
|
+
defaults: Record<string, unknown>;
|
|
25
|
+
workers: RemoteWorkerRecord[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readRemote(): RemoteSettings {
|
|
29
|
+
const section = readConfig().remote;
|
|
30
|
+
return {
|
|
31
|
+
injectStatus: section.injectStatus !== false,
|
|
32
|
+
defaults: (section.defaults as Record<string, unknown>) ?? {},
|
|
33
|
+
workers: (section.workers as RemoteWorkerRecord[]) ?? [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeRemoteWorkers(workers: RemoteWorkerRecord[]): boolean {
|
|
38
|
+
return updateConfig((config) => {
|
|
39
|
+
config.remote.workers = workers;
|
|
40
|
+
});
|
|
41
|
+
}
|