@giovannijecha/jecode 0.3.2 → 0.4.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 +23 -16
- package/dist/command-settings.js +19 -0
- package/dist/commands.js +8 -12
- package/dist/credential-commands.js +63 -80
- package/dist/credentials.js +7 -1
- package/dist/model-command.js +171 -0
- package/dist/permission-command.js +52 -53
- package/dist/provider-commands.js +71 -228
- package/dist/provider-errors.js +4 -3
- package/dist/provider-label.js +2 -2
- package/dist/settings-command.js +43 -98
- package/dist/tui/app-workflows.js +10 -5
- package/dist/tui/app.js +1 -1
- package/dist/tui/components/composer.js +1 -1
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/components/menu.js +27 -15
- package/dist/tui/components/messages.js +2 -2
- package/dist/tui/components/prompt.js +2 -2
- package/dist/tui/components/status.js +2 -2
- package/dist/tui/components/tool.js +3 -3
- package/dist/tui/editor.js +54 -7
- package/dist/tui/feedback.js +2 -2
- package/dist/tui/field.js +1 -1
- package/dist/tui/help.js +4 -1
- package/dist/tui/input.js +4 -0
- package/dist/tui/keys.js +14 -3
- package/dist/tui/overlay.js +6 -0
- package/dist/tui/picker.js +26 -10
- package/dist/tui/session-view.js +2 -2
- package/dist/tui/view.js +1 -1
- package/dist/ui/inline.js +2 -2
- package/dist/ui/markdown.js +7 -7
- package/dist/ui/theme.js +4 -4
- package/package.json +1 -1
package/dist/settings-command.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// The persistent settings hub.
|
|
2
|
-
//
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { EFFORTS, readSettings, settingsLabel, updateSettings } from "./settings.js";
|
|
1
|
+
// The persistent settings hub. Provider access and model selection reuse the
|
|
2
|
+
// same command flows exposed directly through /providers and /models.
|
|
3
|
+
import { saveCommandSettings } from "./command-settings.js";
|
|
4
|
+
import { modelsCommand } from "./model-command.js";
|
|
6
5
|
import { providerFailure } from "./provider-errors.js";
|
|
7
|
-
import {
|
|
6
|
+
import { providerLabel } from "./provider-label.js";
|
|
7
|
+
import { providersCommand } from "./provider-commands.js";
|
|
8
|
+
import { EFFORTS } from "./settings.js";
|
|
8
9
|
import { of } from "./tui/editor.js";
|
|
9
10
|
import { heading } from "./tui/picker.js";
|
|
10
11
|
/** A focused path to the same saved reasoning default exposed by /settings. */
|
|
@@ -22,7 +23,7 @@ export async function settingsCommand(session, host) {
|
|
|
22
23
|
while (true) {
|
|
23
24
|
const values = settingsValues(session);
|
|
24
25
|
const items = settingsItems(values);
|
|
25
|
-
const index = await choose(settingsPicker(values,
|
|
26
|
+
const index = await choose(settingsPicker(values, selected));
|
|
26
27
|
if (index === undefined)
|
|
27
28
|
return;
|
|
28
29
|
const action = items[index]?.action;
|
|
@@ -30,14 +31,8 @@ export async function settingsCommand(session, host) {
|
|
|
30
31
|
return;
|
|
31
32
|
selected = index;
|
|
32
33
|
switch (action) {
|
|
33
|
-
case "provider":
|
|
34
|
-
await providerSetting(session, host);
|
|
35
|
-
break;
|
|
36
|
-
case "ollamaConnection":
|
|
37
|
-
await ollamaConnectionSetting(session, host, (patch) => persist(host, patch));
|
|
38
|
-
break;
|
|
39
34
|
case "model":
|
|
40
|
-
await
|
|
35
|
+
await modelsCommand(session, host, { announce: false });
|
|
41
36
|
break;
|
|
42
37
|
case "effort":
|
|
43
38
|
await effortSetting(session, host);
|
|
@@ -51,45 +46,43 @@ export async function settingsCommand(session, host) {
|
|
|
51
46
|
case "reducedMotion":
|
|
52
47
|
await motionSetting(session, host);
|
|
53
48
|
break;
|
|
54
|
-
case "
|
|
55
|
-
await
|
|
49
|
+
case "providers":
|
|
50
|
+
await providersCommand(session, host);
|
|
56
51
|
break;
|
|
57
52
|
}
|
|
58
53
|
}
|
|
59
54
|
}
|
|
60
|
-
export function settingsPicker(values,
|
|
55
|
+
export function settingsPicker(values, index = 0) {
|
|
61
56
|
return {
|
|
62
|
-
title:
|
|
63
|
-
description: "Changes apply now · flags and environment win at launch",
|
|
57
|
+
title: [],
|
|
64
58
|
options: settingsItems(values).map((item) => item.option),
|
|
65
59
|
index,
|
|
66
60
|
};
|
|
67
61
|
}
|
|
68
62
|
function settingsItems(values) {
|
|
69
63
|
return [
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{ action: "
|
|
78
|
-
{ action: "effort", option: { label: "effort", hint: values.effort } },
|
|
64
|
+
{
|
|
65
|
+
action: "model",
|
|
66
|
+
option: {
|
|
67
|
+
label: "model",
|
|
68
|
+
value: `${providerLabel(values.provider)} · ${values.model || "choose a model"}`,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{ action: "effort", option: { label: "effort", value: values.effort } },
|
|
79
72
|
...(values.maxTokens === undefined
|
|
80
73
|
? []
|
|
81
74
|
: [{
|
|
82
75
|
action: "maxTokens",
|
|
83
|
-
option: { label: "max output tokens",
|
|
76
|
+
option: { label: "max output tokens", value: String(values.maxTokens) },
|
|
84
77
|
}]),
|
|
85
|
-
{ action: "maxSteps", option: { label: "max tool steps",
|
|
78
|
+
{ action: "maxSteps", option: { label: "max tool steps", value: String(values.maxSteps) } },
|
|
86
79
|
{
|
|
87
80
|
action: "reducedMotion",
|
|
88
|
-
option: { label: "reduced motion",
|
|
81
|
+
option: { label: "reduced motion", value: values.reducedMotion ? "on" : "off" },
|
|
89
82
|
},
|
|
90
83
|
{
|
|
91
|
-
action: "
|
|
92
|
-
option: { label: "
|
|
84
|
+
action: "providers",
|
|
85
|
+
option: { label: "providers", hint: "manage access and connections" },
|
|
93
86
|
},
|
|
94
87
|
];
|
|
95
88
|
}
|
|
@@ -97,65 +90,18 @@ function settingsValues(session) {
|
|
|
97
90
|
return {
|
|
98
91
|
provider: session.provider.id,
|
|
99
92
|
model: session.model,
|
|
100
|
-
...(session.provider.id === "ollama" ? { ollamaConnection: ollamaConnectionHint() } : {}),
|
|
101
93
|
effort: session.config.effort,
|
|
102
94
|
...(session.provider.id === "openai-codex" ? {} : { maxTokens: session.config.maxTokens }),
|
|
103
95
|
maxSteps: session.config.maxSteps,
|
|
104
96
|
reducedMotion: session.config.reducedMotion,
|
|
105
97
|
};
|
|
106
98
|
}
|
|
107
|
-
async function providerSetting(session, host) {
|
|
108
|
-
const before = {
|
|
109
|
-
provider: session.provider,
|
|
110
|
-
model: session.model,
|
|
111
|
-
providerId: session.config.providerId,
|
|
112
|
-
configModel: session.config.model,
|
|
113
|
-
effort: session.config.effort,
|
|
114
|
-
};
|
|
115
|
-
if (!(await providersCommand(session, host, { announce: false, save: false })))
|
|
116
|
-
return;
|
|
117
|
-
const current = readSettings();
|
|
118
|
-
const models = { ...current.models };
|
|
119
|
-
if (session.model !== "")
|
|
120
|
-
models[session.provider.id] = session.model;
|
|
121
|
-
const patch = {
|
|
122
|
-
provider: session.provider.id,
|
|
123
|
-
models,
|
|
124
|
-
...(session.config.effort === before.effort ? {} : { effort: session.config.effort }),
|
|
125
|
-
};
|
|
126
|
-
if (await persist(host, patch))
|
|
127
|
-
return;
|
|
128
|
-
session.provider = before.provider;
|
|
129
|
-
session.model = before.model;
|
|
130
|
-
session.config.providerId = before.providerId;
|
|
131
|
-
session.config.model = before.configModel;
|
|
132
|
-
session.config.effort = before.effort;
|
|
133
|
-
}
|
|
134
|
-
async function modelSetting(session, host) {
|
|
135
|
-
const before = {
|
|
136
|
-
model: session.model,
|
|
137
|
-
configModel: session.config.model,
|
|
138
|
-
effort: session.config.effort,
|
|
139
|
-
};
|
|
140
|
-
if (!(await modelsCommand(session, host, { announce: false, save: false })))
|
|
141
|
-
return;
|
|
142
|
-
const current = readSettings();
|
|
143
|
-
const models = { ...current.models, [session.provider.id]: session.model };
|
|
144
|
-
const patch = {
|
|
145
|
-
models,
|
|
146
|
-
...(session.config.effort === before.effort ? {} : { effort: session.config.effort }),
|
|
147
|
-
};
|
|
148
|
-
if (await persist(host, patch))
|
|
149
|
-
return;
|
|
150
|
-
session.model = before.model;
|
|
151
|
-
session.config.model = before.configModel;
|
|
152
|
-
session.config.effort = before.effort;
|
|
153
|
-
}
|
|
154
99
|
async function effortSetting(session, host) {
|
|
155
100
|
const choose = chooser(host);
|
|
156
101
|
if (choose === undefined)
|
|
157
102
|
return;
|
|
158
103
|
const efforts = await availableEfforts(session, host);
|
|
104
|
+
throwIfAborted(host.signal);
|
|
159
105
|
if (efforts === undefined)
|
|
160
106
|
return;
|
|
161
107
|
if (efforts.length === 0) {
|
|
@@ -168,12 +114,12 @@ async function effortSetting(session, host) {
|
|
|
168
114
|
}
|
|
169
115
|
const current = session.config.effort;
|
|
170
116
|
const index = await choose({
|
|
171
|
-
title:
|
|
117
|
+
title: [],
|
|
172
118
|
options: efforts.map((value) => ({ label: value })),
|
|
173
119
|
index: Math.max(0, efforts.findIndex((value) => value === current)),
|
|
174
120
|
});
|
|
175
121
|
const value = index === undefined ? undefined : efforts[index];
|
|
176
|
-
if (value === undefined || !(await
|
|
122
|
+
if (value === undefined || !(await saveCommandSettings(host, { effort: value })))
|
|
177
123
|
return;
|
|
178
124
|
session.config.effort = value;
|
|
179
125
|
return value;
|
|
@@ -181,11 +127,15 @@ async function effortSetting(session, host) {
|
|
|
181
127
|
async function availableEfforts(session, host) {
|
|
182
128
|
if (session.provider.efforts === undefined)
|
|
183
129
|
return EFFORTS;
|
|
184
|
-
host.status?.(`Asking ${session.provider.id}`);
|
|
130
|
+
host.status?.(`Asking ${providerLabel(session.provider.id)}`);
|
|
185
131
|
try {
|
|
186
|
-
|
|
132
|
+
throwIfAborted(host.signal);
|
|
133
|
+
const efforts = await session.provider.efforts(session.model, host.signal, (status) => host.status?.(status));
|
|
134
|
+
throwIfAborted(host.signal);
|
|
135
|
+
return efforts;
|
|
187
136
|
}
|
|
188
137
|
catch (error) {
|
|
138
|
+
throwIfAborted(host.signal);
|
|
189
139
|
host.emit({
|
|
190
140
|
kind: "notice",
|
|
191
141
|
text: providerFailure(session.provider, error, true),
|
|
@@ -203,12 +153,12 @@ async function motionSetting(session, host) {
|
|
|
203
153
|
return;
|
|
204
154
|
const values = [false, true];
|
|
205
155
|
const index = await choose({
|
|
206
|
-
title:
|
|
156
|
+
title: [],
|
|
207
157
|
options: values.map((value) => ({ label: value ? "on" : "off" })),
|
|
208
158
|
index: session.config.reducedMotion ? 1 : 0,
|
|
209
159
|
});
|
|
210
160
|
const value = index === undefined ? undefined : values[index];
|
|
211
|
-
if (value === undefined || !(await
|
|
161
|
+
if (value === undefined || !(await saveCommandSettings(host, { reducedMotion: value })))
|
|
212
162
|
return;
|
|
213
163
|
session.config.reducedMotion = value;
|
|
214
164
|
host.refreshSettings?.();
|
|
@@ -231,23 +181,18 @@ async function numberSetting(session, host, name, label) {
|
|
|
231
181
|
host.emit({ kind: "notice", text: `${label} must be a positive integer`, tone: "error" });
|
|
232
182
|
return;
|
|
233
183
|
}
|
|
234
|
-
if (!(await
|
|
184
|
+
if (!(await saveCommandSettings(host, { [name]: value })))
|
|
235
185
|
return;
|
|
236
186
|
session.config[name] = value;
|
|
237
187
|
}
|
|
238
|
-
async function persist(host, patch) {
|
|
239
|
-
try {
|
|
240
|
-
await updateSettings(patch);
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
catch (error) {
|
|
244
|
-
host.emit({ kind: "notice", text: `could not save settings · ${error.message}`, tone: "error" });
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
188
|
function chooser(host) {
|
|
249
189
|
if (host.choose === undefined) {
|
|
250
190
|
host.emit({ kind: "notice", text: "that command needs the screen", tone: "warn" });
|
|
251
191
|
}
|
|
252
192
|
return host.choose;
|
|
253
193
|
}
|
|
194
|
+
function throwIfAborted(signal) {
|
|
195
|
+
if (signal?.aborted !== true)
|
|
196
|
+
return;
|
|
197
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("interrupted");
|
|
198
|
+
}
|
|
@@ -59,11 +59,16 @@ export function appWorkflows(options) {
|
|
|
59
59
|
state.closeWhenIdle = true;
|
|
60
60
|
}
|
|
61
61
|
catch (error) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
// Command cancellation is already visible through the dock closing and
|
|
63
|
+
// the activity ending. Keep it silent instead of replacing the footer
|
|
64
|
+
// with a redundant warning.
|
|
65
|
+
if (!activity.control.signal.aborted) {
|
|
66
|
+
feedback.show({
|
|
67
|
+
text: error.message,
|
|
68
|
+
tone: "error",
|
|
69
|
+
timeoutMs: 6_000,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
73
|
finally {
|
|
69
74
|
options.finishActivity(activity);
|
package/dist/tui/app.js
CHANGED
|
@@ -26,7 +26,7 @@ const ESCAPE_MS = 25;
|
|
|
26
26
|
export async function runApp(session, transcriptRoot, environment = {}) {
|
|
27
27
|
const terminal = environment.screen ?? realScreen;
|
|
28
28
|
const paint = environment.paint ?? painter();
|
|
29
|
-
const keys = decoder();
|
|
29
|
+
const keys = decoder({ ctrlBackspaceIsBs: process.env["WT_SESSION"] !== undefined });
|
|
30
30
|
const transcript = transcriptRenderer();
|
|
31
31
|
const workspace = await workspaceLabel(session.config.root);
|
|
32
32
|
const state = appState();
|
|
@@ -7,7 +7,7 @@ export function renderComposer(editor, width, maxInputRows, pal, right = "") {
|
|
|
7
7
|
const first = Math.max(0, Math.min(laid.lines.length - room, laid.cursor.row));
|
|
8
8
|
const shown = laid.lines.slice(first, first + room);
|
|
9
9
|
return {
|
|
10
|
-
rows: shown.map((line, index) => row(width, [{ text: line, fg: pal.ink.bright }], index === 0 && right !== "" ? [{ text: right, fg: pal.ink.
|
|
10
|
+
rows: shown.map((line, index) => row(width, [{ text: line, fg: pal.ink.bright }], index === 0 && right !== "" ? [{ text: right, fg: pal.ink.dim }] : [])),
|
|
11
11
|
cursor: { row: laid.cursor.row - first, col: laid.cursor.col },
|
|
12
12
|
};
|
|
13
13
|
}
|
|
@@ -5,7 +5,7 @@ export function renderFooter(info, status, width, pal) {
|
|
|
5
5
|
const right = fitSegs(status, rightLimit);
|
|
6
6
|
const leftRoom = Math.max(0, width - plainLen(right) - (right.length === 0 ? 0 : 1));
|
|
7
7
|
const left = identity(info, leftRoom);
|
|
8
|
-
return [row(width, left === "" ? [] : [{ text: left, fg: pal.ink.
|
|
8
|
+
return [row(width, left === "" ? [] : [{ text: left, fg: pal.ink.dim }], right)];
|
|
9
9
|
}
|
|
10
10
|
function identity(info, cols) {
|
|
11
11
|
if (cols <= 0)
|
|
@@ -16,28 +16,40 @@ export function menuWindow(length, selected, visible) {
|
|
|
16
16
|
return { first, last: Math.min(count, first + room) };
|
|
17
17
|
}
|
|
18
18
|
function renderEntry(entry, labelWidth, width, pal) {
|
|
19
|
-
// Colour terminals
|
|
20
|
-
//
|
|
21
|
-
//
|
|
19
|
+
// Colour terminals spend focus on the active label instead of painting a
|
|
20
|
+
// full-width band. Monochrome has no colour, so it alone reserves a fixed
|
|
21
|
+
// arrow column to keep selection visible without shifting peer rows.
|
|
22
22
|
const monochrome = !hasColor();
|
|
23
23
|
const selectedMark = monochrome ? (entry.selected ? "→ " : " ") : "";
|
|
24
|
-
const fg = entry.selected ? pal.
|
|
24
|
+
const fg = entry.selected ? pal.focus : pal.ink.fg;
|
|
25
25
|
const primary = [
|
|
26
|
-
{ text: selectedMark, fg
|
|
27
|
-
{ text: entry.label, fg },
|
|
26
|
+
{ text: selectedMark, fg },
|
|
27
|
+
{ text: entry.label, fg, bold: entry.selected || undefined },
|
|
28
28
|
];
|
|
29
29
|
if (width > 40 && entry.description !== undefined) {
|
|
30
30
|
const gap = Math.max(2, labelWidth - primaryWidth(entry));
|
|
31
|
-
primary.push({
|
|
31
|
+
primary.push({
|
|
32
|
+
text: `${" ".repeat(gap)}${entry.description}`,
|
|
33
|
+
fg: entry.selected ? pal.ink.bright : pal.ink.muted,
|
|
34
|
+
});
|
|
32
35
|
}
|
|
33
|
-
const
|
|
34
|
-
?
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const value = entry.value === undefined
|
|
37
|
+
? undefined
|
|
38
|
+
: entry.selected && entry.adjustable === true
|
|
39
|
+
? `‹ ${entry.value} ›`
|
|
40
|
+
: entry.value;
|
|
41
|
+
const summary = [entry.hint, value]
|
|
42
|
+
.filter((part) => part !== undefined)
|
|
43
|
+
.join(" · ");
|
|
44
|
+
const rightColor = entry.selected ? pal.focus : pal.ink.muted;
|
|
45
|
+
const right = summary === "" || (width <= 40 && entry.value === undefined)
|
|
46
|
+
? []
|
|
47
|
+
: [{
|
|
48
|
+
text: elide(summary, Math.max(1, Math.floor(entry.value === undefined ? width / 4 : width * 0.45))),
|
|
49
|
+
fg: rightColor,
|
|
50
|
+
bold: entry.selected || undefined,
|
|
51
|
+
}];
|
|
52
|
+
return row(width, primary, right);
|
|
41
53
|
}
|
|
42
54
|
function primaryWidth(entry) {
|
|
43
55
|
return plainLen([{ text: entry.label }]);
|
|
@@ -46,8 +46,8 @@ export function renderReasoning(block, width, pal) {
|
|
|
46
46
|
fg: pal.ink.bright,
|
|
47
47
|
bold: true,
|
|
48
48
|
},
|
|
49
|
-
], action === undefined ? [] : [{ text: action, fg: pal.ink.
|
|
50
|
-
...visible.map((line) => row(width, line.segs.map((seg) => ({ ...seg, fg: pal.ink.
|
|
49
|
+
], action === undefined ? [] : [{ text: action, fg: pal.ink.dim }], undefined, PAD),
|
|
50
|
+
...visible.map((line) => row(width, line.segs.map((seg) => ({ ...seg, fg: pal.ink.dim, italic: true })), [], undefined, PAD)),
|
|
51
51
|
];
|
|
52
52
|
}
|
|
53
53
|
export function reasoningPreviewSource(text, width) {
|
|
@@ -7,10 +7,10 @@ export const PROMPT_WIDTH = textWidth(MARK);
|
|
|
7
7
|
export function promptLine(text, cursor, width, pal, options = {}) {
|
|
8
8
|
const right = options.right === undefined || options.right === ""
|
|
9
9
|
? []
|
|
10
|
-
: [{ text: options.right, fg: pal.ink.
|
|
10
|
+
: [{ text: options.right, fg: pal.ink.dim }];
|
|
11
11
|
const laid = layout(text, cursor, width, options);
|
|
12
12
|
const content = laid.visible === "" && options.placeholder !== undefined
|
|
13
|
-
? { text: options.placeholder, fg: pal.ink.
|
|
13
|
+
? { text: options.placeholder, fg: pal.ink.dim }
|
|
14
14
|
: { text: laid.visible, fg: pal.ink.bright };
|
|
15
15
|
return {
|
|
16
16
|
row: row(width, [{ text: MARK, fg: pal.accent }, content], right),
|
|
@@ -6,7 +6,7 @@ export function renderStatus(info, pal) {
|
|
|
6
6
|
if (urgent !== undefined)
|
|
7
7
|
return withUnseen(feedbackSegments(urgent, pal), info.unseen, pal);
|
|
8
8
|
if (info.status !== undefined) {
|
|
9
|
-
return withUnseen([{ text: "esc to interrupt", fg: pal.ink.
|
|
9
|
+
return withUnseen([{ text: "esc to interrupt", fg: pal.ink.dim }], info.unseen, pal);
|
|
10
10
|
}
|
|
11
11
|
if (info.feedback !== undefined) {
|
|
12
12
|
return withUnseen(feedbackSegments(info.feedback, pal), info.unseen, pal);
|
|
@@ -30,7 +30,7 @@ function feedbackSegments(feedback, pal) {
|
|
|
30
30
|
function withUnseen(status, unseen, pal) {
|
|
31
31
|
return unseen === 0
|
|
32
32
|
? status
|
|
33
|
-
: [...status, { text: " · ", fg: pal.ink.
|
|
33
|
+
: [...status, { text: " · ", fg: pal.ink.dim }, ...unseenSegments(unseen, pal)];
|
|
34
34
|
}
|
|
35
35
|
function unseenSegments(unseen, pal) {
|
|
36
36
|
return [{ text: `${unseen} new ↓`, fg: pal.accent, bold: true }];
|
|
@@ -11,7 +11,7 @@ export function renderTool(block, width, pal, context = {}) {
|
|
|
11
11
|
{ text: " " },
|
|
12
12
|
{ text: `${stateGlyph(block, context)} `, fg: statusInk(block.tone, pal), bold: true },
|
|
13
13
|
{ text: block.name, fg: pal.ink.bright, bold: true },
|
|
14
|
-
...(block.target === "" ? [] : [{ text: ` ${block.target}`, fg: pal.
|
|
14
|
+
...(block.target === "" ? [] : [{ text: ` ${block.target}`, fg: pal.technical }]),
|
|
15
15
|
], right === "" ? [] : [{ text: right, fg: statusInk(block.tone, pal) }]),
|
|
16
16
|
...shown.map((detail) => renderDetail(detail, block.tone, width, pal)),
|
|
17
17
|
];
|
|
@@ -42,7 +42,7 @@ function renderDetail(detail, tone, width, pal) {
|
|
|
42
42
|
return row(width, [lead, rail, { text: detail.text === "" ? " " : detail.text, fg }]);
|
|
43
43
|
}
|
|
44
44
|
if (detail.kind === "gap") {
|
|
45
|
-
return row(width, [lead, rail, { text: detail.text, fg: pal.ink.
|
|
45
|
+
return row(width, [lead, rail, { text: detail.text, fg: pal.ink.dim, italic: true }]);
|
|
46
46
|
}
|
|
47
47
|
const number = detail.kind === "add" ? detail.newLine : detail.oldLine;
|
|
48
48
|
const prefix = `${detail.kind === "add" ? "+" : detail.kind === "del" ? "-" : " "}${String(number ?? "").padStart(3)} `;
|
|
@@ -50,7 +50,7 @@ function renderDetail(detail, tone, width, pal) {
|
|
|
50
50
|
? pal.ink.added
|
|
51
51
|
: detail.kind === "del"
|
|
52
52
|
? pal.ink.removed
|
|
53
|
-
: pal.ink.
|
|
53
|
+
: pal.ink.dim;
|
|
54
54
|
return row(width, [lead, rail, { text: prefix, fg }, ...emphasized(detail.text, detail.emphasis, fg)]);
|
|
55
55
|
}
|
|
56
56
|
function emphasized(text, emphasis, fg) {
|
package/dist/tui/editor.js
CHANGED
|
@@ -43,15 +43,25 @@ export function wordLeft(state) {
|
|
|
43
43
|
return { ...state, cursor: startOfWordBefore(state.text, state.cursor) };
|
|
44
44
|
}
|
|
45
45
|
export function wordRight(state) {
|
|
46
|
-
return { ...state, cursor:
|
|
46
|
+
return { ...state, cursor: startOfWordAfter(state.text, state.cursor) };
|
|
47
47
|
}
|
|
48
48
|
/** ctrl+w — delete the word behind the cursor. */
|
|
49
49
|
export function killWord(state) {
|
|
50
|
-
const from =
|
|
50
|
+
const from = startOfDeletionBefore(state.text, state.cursor);
|
|
51
51
|
if (from === state.cursor)
|
|
52
52
|
return state;
|
|
53
53
|
return { text: state.text.slice(0, from) + state.text.slice(state.cursor), cursor: from };
|
|
54
54
|
}
|
|
55
|
+
/** ctrl+delete — delete the word ahead of the cursor. */
|
|
56
|
+
export function killNextWord(state) {
|
|
57
|
+
const to = endOfDeletionAfter(state.text, state.cursor);
|
|
58
|
+
if (to === state.cursor)
|
|
59
|
+
return state;
|
|
60
|
+
return {
|
|
61
|
+
text: state.text.slice(0, state.cursor) + state.text.slice(to),
|
|
62
|
+
cursor: state.cursor,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
55
65
|
/** ctrl+u — delete everything behind the cursor. */
|
|
56
66
|
export function killToStart(state) {
|
|
57
67
|
return { text: state.text.slice(state.cursor), cursor: 0 };
|
|
@@ -89,17 +99,54 @@ function after(text, cursor) {
|
|
|
89
99
|
}
|
|
90
100
|
function startOfWordBefore(text, cursor) {
|
|
91
101
|
let i = cursor;
|
|
92
|
-
while (i > 0 && text[i - 1]
|
|
102
|
+
while (i > 0 && whitespace(text[i - 1]))
|
|
93
103
|
i--;
|
|
94
|
-
while (i > 0 && text[i - 1]
|
|
104
|
+
while (i > 0 && !whitespace(text[i - 1]))
|
|
95
105
|
i--;
|
|
96
106
|
return i;
|
|
97
107
|
}
|
|
98
|
-
function
|
|
108
|
+
function startOfWordAfter(text, cursor) {
|
|
99
109
|
let i = cursor;
|
|
100
|
-
while (i < text.length && text[i]
|
|
110
|
+
while (i < text.length && !whitespace(text[i]))
|
|
101
111
|
i++;
|
|
102
|
-
while (i < text.length && text[i]
|
|
112
|
+
while (i < text.length && whitespace(text[i]))
|
|
103
113
|
i++;
|
|
104
114
|
return i;
|
|
105
115
|
}
|
|
116
|
+
function startOfDeletionBefore(text, cursor) {
|
|
117
|
+
if (text[cursor - 1] === "\n")
|
|
118
|
+
return cursor - 1;
|
|
119
|
+
let i = cursor;
|
|
120
|
+
while (i > 0 && horizontalWhitespace(text[i - 1]))
|
|
121
|
+
i--;
|
|
122
|
+
if (text[i - 1] === "\n")
|
|
123
|
+
return i;
|
|
124
|
+
while (i > 0 && !whitespace(text[i - 1]))
|
|
125
|
+
i--;
|
|
126
|
+
return i;
|
|
127
|
+
}
|
|
128
|
+
function endOfDeletionAfter(text, cursor) {
|
|
129
|
+
if (text[cursor] === "\n")
|
|
130
|
+
return cursor + 1;
|
|
131
|
+
let i = cursor;
|
|
132
|
+
if (horizontalWhitespace(text[i])) {
|
|
133
|
+
while (i < text.length && horizontalWhitespace(text[i]))
|
|
134
|
+
i++;
|
|
135
|
+
if (text[i] === "\n")
|
|
136
|
+
return i;
|
|
137
|
+
while (i < text.length && !whitespace(text[i]))
|
|
138
|
+
i++;
|
|
139
|
+
return i;
|
|
140
|
+
}
|
|
141
|
+
while (i < text.length && !whitespace(text[i]))
|
|
142
|
+
i++;
|
|
143
|
+
while (i < text.length && horizontalWhitespace(text[i]))
|
|
144
|
+
i++;
|
|
145
|
+
return i;
|
|
146
|
+
}
|
|
147
|
+
function horizontalWhitespace(value) {
|
|
148
|
+
return value !== undefined && value !== "\n" && /\s/u.test(value);
|
|
149
|
+
}
|
|
150
|
+
function whitespace(value) {
|
|
151
|
+
return value !== undefined && /\s/u.test(value);
|
|
152
|
+
}
|
package/dist/tui/feedback.js
CHANGED
|
@@ -61,8 +61,8 @@ export function turnBlocker(session) {
|
|
|
61
61
|
if (blocked !== undefined) {
|
|
62
62
|
return {
|
|
63
63
|
text: auth.kind === "oauth"
|
|
64
|
-
? `${providerLabel(session.provider.id)} needs
|
|
65
|
-
: `${providerLabel(session.provider.id)} needs an API key · /
|
|
64
|
+
? `${providerLabel(session.provider.id)} needs sign-in · /providers`
|
|
65
|
+
: `${providerLabel(session.provider.id)} needs an API key · /providers`,
|
|
66
66
|
tone: "warn",
|
|
67
67
|
};
|
|
68
68
|
}
|
package/dist/tui/field.js
CHANGED
|
@@ -7,7 +7,7 @@ import { row } from "../ui/render.js";
|
|
|
7
7
|
import { promptCursor, promptLine } from "./components/prompt.js";
|
|
8
8
|
export function panel(field, width, pal) {
|
|
9
9
|
const { ink } = pal;
|
|
10
|
-
const head = row(width, field.title, field.right === undefined ? [] : [{ text: field.right, fg: ink.
|
|
10
|
+
const head = row(width, field.title, field.right === undefined ? [] : [{ text: field.right, fg: ink.dim }]);
|
|
11
11
|
const line = promptLine(field.editor.text, field.editor.cursor, width, pal, {
|
|
12
12
|
secret: field.secret,
|
|
13
13
|
}).row;
|
package/dist/tui/help.js
CHANGED
|
@@ -4,6 +4,9 @@ import { textWidth } from "../ui/width.js";
|
|
|
4
4
|
const CONTROL_WIDTH = 18;
|
|
5
5
|
const CONTROLS = [
|
|
6
6
|
{ key: "up / down", description: "move through menus or history" },
|
|
7
|
+
{ key: "left / right", description: "move cursor or change a value" },
|
|
8
|
+
{ key: "ctrl+left / right", description: "move cursor by word" },
|
|
9
|
+
{ key: "ctrl+backspace/del", description: "delete a word" },
|
|
7
10
|
{ key: "enter / tab", description: "select or send · complete" },
|
|
8
11
|
{ key: "alt+enter", description: "insert a new line" },
|
|
9
12
|
{ key: "esc", description: "close UI or interrupt work" },
|
|
@@ -16,7 +19,7 @@ export function panel(width, pal, maxRows = CONTROLS.length + 1) {
|
|
|
16
19
|
const heading = row(width, [
|
|
17
20
|
{ text: "help ", fg: pal.accent, bold: true },
|
|
18
21
|
{ text: "keyboard controls", fg: pal.ink.fg },
|
|
19
|
-
], [{ text: "esc close", fg: pal.ink.
|
|
22
|
+
], [{ text: "esc close", fg: pal.ink.dim }]);
|
|
20
23
|
const controls = CONTROLS.map((control) => {
|
|
21
24
|
const gap = Math.max(2, CONTROL_WIDTH - textWidth(control.key));
|
|
22
25
|
return row(width, [
|
package/dist/tui/input.js
CHANGED
|
@@ -26,6 +26,10 @@ export function applyKey(state, key) {
|
|
|
26
26
|
return edit.wordLeft(state);
|
|
27
27
|
case "wordright":
|
|
28
28
|
return edit.wordRight(state);
|
|
29
|
+
case "deletewordleft":
|
|
30
|
+
return edit.killWord(state);
|
|
31
|
+
case "deletewordright":
|
|
32
|
+
return edit.killNextWord(state);
|
|
29
33
|
case "home":
|
|
30
34
|
return edit.home(state);
|
|
31
35
|
case "end":
|
package/dist/tui/keys.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// guessed at; and a bracketed paste arrives as a delimited run that must not be
|
|
6
6
|
// interpreted key by key, or a pasted newline submits half the paste.
|
|
7
7
|
const ESC = String.fromCharCode(27);
|
|
8
|
+
const BS = String.fromCharCode(8);
|
|
8
9
|
const DEL = String.fromCharCode(127);
|
|
9
10
|
const PASTE_START = "[200~";
|
|
10
11
|
const PASTE_END = "[201~";
|
|
@@ -35,10 +36,18 @@ const SEQUENCES = {
|
|
|
35
36
|
"[7~": "home",
|
|
36
37
|
"[8~": "end",
|
|
37
38
|
"[3~": "delete",
|
|
39
|
+
"[3;5~": "deletewordright",
|
|
38
40
|
"[5~": "pageup",
|
|
39
41
|
"[6~": "pagedown",
|
|
40
42
|
"[1;5C": "wordright",
|
|
41
43
|
"[1;5D": "wordleft",
|
|
44
|
+
// Explicit modified-key forms used by terminals with CSI-u support.
|
|
45
|
+
"[127;5u": "deletewordleft",
|
|
46
|
+
"[8;5u": "deletewordleft",
|
|
47
|
+
// Readline-compatible Alt bindings. VS Code also sends Alt+D for
|
|
48
|
+
// Ctrl+Delete and Ctrl+W for Ctrl+Backspace in its integrated terminal.
|
|
49
|
+
d: "deletewordright",
|
|
50
|
+
[DEL]: "deletewordleft",
|
|
42
51
|
"[Z": "backtab",
|
|
43
52
|
// alt+enter, which arrives as an escape followed by the return itself. It is
|
|
44
53
|
// how a multi-line message gets written when enter is what sends one.
|
|
@@ -48,10 +57,10 @@ const CONTROL = {
|
|
|
48
57
|
"\r": "enter",
|
|
49
58
|
"\n": "enter",
|
|
50
59
|
"\t": "tab",
|
|
51
|
-
|
|
60
|
+
[BS]: "backspace",
|
|
52
61
|
[DEL]: "backspace",
|
|
53
62
|
};
|
|
54
|
-
export function decoder() {
|
|
63
|
+
export function decoder(options = {}) {
|
|
55
64
|
let held = "";
|
|
56
65
|
let pasting = false;
|
|
57
66
|
let pasted = "";
|
|
@@ -121,7 +130,9 @@ export function decoder() {
|
|
|
121
130
|
keys.push({ name: "escape", text: "", ctrl: false });
|
|
122
131
|
continue;
|
|
123
132
|
}
|
|
124
|
-
const named =
|
|
133
|
+
const named = ch === BS && options.ctrlBackspaceIsBs === true
|
|
134
|
+
? "deletewordleft"
|
|
135
|
+
: CONTROL[ch];
|
|
125
136
|
if (named !== undefined) {
|
|
126
137
|
held = held.slice(1);
|
|
127
138
|
keys.push({ name: named, text: "", ctrl: false });
|
package/dist/tui/overlay.js
CHANGED
|
@@ -47,6 +47,12 @@ function handlePicker(open, key) {
|
|
|
47
47
|
case "down":
|
|
48
48
|
open.picker = picker.move(open.picker, 1);
|
|
49
49
|
break;
|
|
50
|
+
case "left":
|
|
51
|
+
open.picker = picker.adjust(open.picker, -1);
|
|
52
|
+
break;
|
|
53
|
+
case "right":
|
|
54
|
+
open.picker = picker.adjust(open.picker, 1);
|
|
55
|
+
break;
|
|
50
56
|
case "home":
|
|
51
57
|
open.picker = picker.edge(open.picker, "home");
|
|
52
58
|
break;
|