@stevezhou/sisu 0.1.11 → 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/NOTICE +12 -0
- package/README.md +3 -2
- package/dist/commands.js +36 -84
- package/dist/main.js +7 -5
- package/dist/pager/app.js +3 -3
- package/dist/pager/input.js +43 -20
- package/dist/pager/model.js +94 -25
- package/dist/runtime/adapter.js +164 -0
- package/dist/runtime/index.js +33 -0
- package/dist/runtime/launch.js +94 -0
- package/dist/runtime/loop.js +91 -0
- package/dist/runtime/models.js +56 -0
- package/dist/runtime/sessions.js +65 -0
- package/dist/runtime/suite.js +64 -0
- package/dist/runtime/tools.js +256 -0
- package/dist/runtime/transport.js +93 -0
- package/dist/runtime/types.js +2 -0
- package/dist/tui.js +17 -3
- package/package.json +4 -3
package/NOTICE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
SiSu CLI
|
|
2
|
+
Copyright 2026 SiSu / 思溯
|
|
3
|
+
|
|
4
|
+
This product includes grok-build first-party source (Apache-2.0)
|
|
5
|
+
from https://github.com/xai-org/grok-build, Copyright 2023-2026 SpaceXAI.
|
|
6
|
+
|
|
7
|
+
The grok-build tree lives at vendor/grok-build/ together with its
|
|
8
|
+
LICENSE and THIRD-PARTY-NOTICES. SiSu二次开发 replaces xAI login,
|
|
9
|
+
telemetry defaults, branding, and the model/quota endpoint so the
|
|
10
|
+
user-facing binary is SiSu (`sisu`), not the official `grok` client.
|
|
11
|
+
|
|
12
|
+
See vendor/grok-build/NOTICE for the grok-build-specific attribution.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Official publish channel for the SiSu CLI npm package.
|
|
4
4
|
|
|
5
|
-
One login. Cloud quota. Local
|
|
5
|
+
One login. Cloud quota. Local runtime. Auth lives in `~/.sisu`, shared with SiSu Desktop. The CLI is a grok-build-derived local coding agent (read / edit / search / shell + the grok-build extension surface). SiSu cloud plays the Claude/Grok role: device login, model list, billed completions (`POST /api/runtime/complete`). Sessions are local under `~/.sisu/sessions`.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -29,9 +29,10 @@ Default API is `https://www.sisu.chat`. Override with `--api` or `SISU_API_BASE`
|
|
|
29
29
|
## Commands
|
|
30
30
|
|
|
31
31
|
```
|
|
32
|
-
sisu interactive TUI
|
|
32
|
+
sisu interactive TUI (local runtime; Grok Build if the pager binary is present)
|
|
33
33
|
sisu open <dir> --project <id>
|
|
34
34
|
sisu exec "<prompt>"
|
|
35
|
+
sisu -p "<prompt>"
|
|
35
36
|
sisu history
|
|
36
37
|
sisu logout
|
|
37
38
|
```
|
package/dist/commands.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveRuntimeModel = exports.resolveCatalogModel = exports.fetchModelCatalog = void 0;
|
|
6
7
|
exports.loginCommand = loginCommand;
|
|
7
8
|
exports.logoutCommand = logoutCommand;
|
|
8
9
|
exports.resolveVerificationUrl = resolveVerificationUrl;
|
|
@@ -18,16 +19,19 @@ exports.execCommand = execCommand;
|
|
|
18
19
|
exports.listConversationsCommand = listConversationsCommand;
|
|
19
20
|
exports.openConversationCommand = openConversationCommand;
|
|
20
21
|
exports.setTrainingCommand = setTrainingCommand;
|
|
21
|
-
exports.fetchModelCatalog = fetchModelCatalog;
|
|
22
|
-
exports.resolveCatalogModel = resolveCatalogModel;
|
|
23
22
|
exports.listModelsCommand = listModelsCommand;
|
|
24
23
|
exports.setModelCommand = setModelCommand;
|
|
25
24
|
const child_process_1 = require("child_process");
|
|
26
25
|
const fs_1 = __importDefault(require("fs"));
|
|
27
26
|
const path_1 = __importDefault(require("path"));
|
|
28
27
|
const http_1 = require("./http");
|
|
29
|
-
const
|
|
30
|
-
const
|
|
28
|
+
const adapter_1 = require("./runtime/adapter");
|
|
29
|
+
const loop_1 = require("./runtime/loop");
|
|
30
|
+
const models_1 = require("./runtime/models");
|
|
31
|
+
Object.defineProperty(exports, "fetchModelCatalog", { enumerable: true, get: function () { return models_1.fetchModelCatalog; } });
|
|
32
|
+
Object.defineProperty(exports, "resolveCatalogModel", { enumerable: true, get: function () { return models_1.resolveCatalogModel; } });
|
|
33
|
+
Object.defineProperty(exports, "resolveRuntimeModel", { enumerable: true, get: function () { return models_1.resolveRuntimeModel; } });
|
|
34
|
+
const transport_1 = require("./runtime/transport");
|
|
31
35
|
const store_1 = require("./store");
|
|
32
36
|
async function loginCommand(input, http = http_1.defaultHttp) {
|
|
33
37
|
const apiBase = (input.apiBase || process.env.SISU_API_BASE || store_1.DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
@@ -266,56 +270,37 @@ function listLocalCommand(projectId) {
|
|
|
266
270
|
}).join('\n');
|
|
267
271
|
}
|
|
268
272
|
async function execCommand(prompt, options = {}, http = http_1.defaultHttp) {
|
|
269
|
-
const auth = (0, store_1.requireAuth)();
|
|
270
273
|
const text = prompt.trim();
|
|
271
274
|
if (!text)
|
|
272
275
|
throw new Error('prompt is required');
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
throw new Error((0, http_1.errorDetail)(body, `create conversation failed (${created.status})`));
|
|
290
|
-
conversationId = String(body.id || '');
|
|
291
|
-
if (!conversationId)
|
|
292
|
-
throw new Error('create conversation missing id');
|
|
293
|
-
}
|
|
294
|
-
const sent = await http(`${auth.api_base}/api/chat/send`, {
|
|
295
|
-
method: 'POST',
|
|
296
|
-
headers: (0, http_1.authHeaders)(auth.token),
|
|
297
|
-
body: JSON.stringify({
|
|
298
|
-
conversation_id: conversationId,
|
|
299
|
-
message: text,
|
|
300
|
-
model: options.model || (0, store_1.readSession)().last_model || undefined,
|
|
301
|
-
task_category: 'coding',
|
|
302
|
-
client: stamp.client,
|
|
303
|
-
client_version: stamp.client_version,
|
|
304
|
-
client_request_id: stamp.client_request_id,
|
|
305
|
-
}),
|
|
306
|
-
});
|
|
307
|
-
if (!sent.ok) {
|
|
308
|
-
const body = await sent.json().catch(() => ({}));
|
|
309
|
-
throw new Error((0, http_1.errorDetail)(body, `exec failed (${sent.status})`));
|
|
276
|
+
const stub = Boolean(options.stub || process.env.SISU_RUNTIME_STUB === '1');
|
|
277
|
+
const cwd = options.cwd || process.cwd();
|
|
278
|
+
const modelClient = options.modelClient || (stub
|
|
279
|
+
? (0, loop_1.createLaunchStubModel)()
|
|
280
|
+
: (() => {
|
|
281
|
+
const auth = (0, store_1.requireAuth)();
|
|
282
|
+
return (0, adapter_1.createSisuCloudModel)(http, {
|
|
283
|
+
apiBase: auth.api_base,
|
|
284
|
+
token: auth.token,
|
|
285
|
+
client: options.client || 'cli',
|
|
286
|
+
});
|
|
287
|
+
})());
|
|
288
|
+
if (!stub)
|
|
289
|
+
(0, store_1.requireAuth)();
|
|
290
|
+
if (options.projectId) {
|
|
291
|
+
(0, store_1.writeSession)({ ...(0, store_1.readSession)(), last_project_id: options.projectId });
|
|
310
292
|
}
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
293
|
+
const model = await (0, models_1.resolveRuntimeModel)(http, { explicit: options.model, stub });
|
|
294
|
+
const result = await (0, transport_1.execLocalTurn)(text, {
|
|
295
|
+
cwd,
|
|
296
|
+
model,
|
|
297
|
+
conversationId: options.conversationId,
|
|
298
|
+
newConversation: options.newConversation,
|
|
299
|
+
modelClient,
|
|
300
|
+
http,
|
|
301
|
+
client: options.client || 'cli',
|
|
317
302
|
});
|
|
318
|
-
return { conversationId, text:
|
|
303
|
+
return { conversationId: result.conversationId, text: result.text };
|
|
319
304
|
}
|
|
320
305
|
async function listConversationsCommand(http = http_1.defaultHttp) {
|
|
321
306
|
const auth = (0, store_1.requireAuth)();
|
|
@@ -352,36 +337,8 @@ async function setTrainingCommand(optIn, http = http_1.defaultHttp) {
|
|
|
352
337
|
throw new Error((0, http_1.errorDetail)(body, `training update failed (${response.status})`));
|
|
353
338
|
return optIn ? 'training opt-in on (new turns may be used if eligible)' : 'training opt-in off';
|
|
354
339
|
}
|
|
355
|
-
function normalizeModelKey(value) {
|
|
356
|
-
return value.toLowerCase().replace(/[-_.\s]/g, '');
|
|
357
|
-
}
|
|
358
|
-
async function fetchModelCatalog(http = http_1.defaultHttp) {
|
|
359
|
-
const auth = (0, store_1.requireAuth)();
|
|
360
|
-
const response = await http(`${auth.api_base}/api/chat/models`, { headers: (0, http_1.authHeaders)(auth.token) });
|
|
361
|
-
const body = await response.json().catch(() => ({}));
|
|
362
|
-
if (!response.ok)
|
|
363
|
-
throw new Error((0, http_1.errorDetail)(body, `models failed (${response.status})`));
|
|
364
|
-
const rows = Array.isArray(body?.models) ? body.models : [];
|
|
365
|
-
const models = rows
|
|
366
|
-
.map((row) => {
|
|
367
|
-
const name = String(row?.name || '').trim();
|
|
368
|
-
if (!name)
|
|
369
|
-
return null;
|
|
370
|
-
return { name, label: String(row.display_name || row.label || name) };
|
|
371
|
-
})
|
|
372
|
-
.filter((row) => Boolean(row));
|
|
373
|
-
return { models, defaultModel: String(body?.default_model || '') };
|
|
374
|
-
}
|
|
375
|
-
function resolveCatalogModel(query, models) {
|
|
376
|
-
const needle = normalizeModelKey(query);
|
|
377
|
-
if (!needle)
|
|
378
|
-
return undefined;
|
|
379
|
-
return (models.find((row) => normalizeModelKey(row.name) === needle) ||
|
|
380
|
-
models.find((row) => normalizeModelKey(row.label) === needle) ||
|
|
381
|
-
models.find((row) => normalizeModelKey(row.name).includes(needle) || normalizeModelKey(row.label).includes(needle)));
|
|
382
|
-
}
|
|
383
340
|
async function listModelsCommand(http = http_1.defaultHttp) {
|
|
384
|
-
const { models, defaultModel } = await fetchModelCatalog(http);
|
|
341
|
+
const { models, defaultModel } = await (0, models_1.fetchModelCatalog)(http);
|
|
385
342
|
const current = (0, store_1.readSession)().last_model || defaultModel;
|
|
386
343
|
if (!current && !models.length)
|
|
387
344
|
return 'no models available';
|
|
@@ -399,11 +356,6 @@ async function setModelCommand(query, http = http_1.defaultHttp) {
|
|
|
399
356
|
const wanted = query.trim();
|
|
400
357
|
if (!wanted)
|
|
401
358
|
return listModelsCommand(http);
|
|
402
|
-
const
|
|
403
|
-
const match = resolveCatalogModel(wanted, models);
|
|
404
|
-
const name = match?.name || (normalizeModelKey(wanted) === normalizeModelKey(defaultModel) ? defaultModel : '');
|
|
405
|
-
if (!name)
|
|
406
|
-
throw new Error(`unknown model ${wanted}`);
|
|
407
|
-
(0, store_1.writeSession)({ ...(0, store_1.readSession)(), last_model: name });
|
|
359
|
+
const name = await (0, models_1.resolveRuntimeModel)(http, { explicit: wanted });
|
|
408
360
|
return `model ${name}`;
|
|
409
361
|
}
|
package/dist/main.js
CHANGED
|
@@ -24,13 +24,14 @@ Usage:
|
|
|
24
24
|
sisu status
|
|
25
25
|
sisu open <dir> --project <project-id>
|
|
26
26
|
sisu ls [--project <project-id>]
|
|
27
|
-
sisu exec "<prompt>" [--project <id>] [--model <name>] [--new]
|
|
27
|
+
sisu exec "<prompt>" [--project <id>] [--model <name>] [--new] [--stub]
|
|
28
|
+
sisu -p "<prompt>" headless local-agent turn (alias of exec)
|
|
28
29
|
sisu models
|
|
29
30
|
sisu model <name>
|
|
30
31
|
sisu history
|
|
31
32
|
sisu thread <conversation-id>
|
|
32
33
|
sisu training --on|--off
|
|
33
|
-
sisu
|
|
34
|
+
sisu Grok Build TUI (SiSu account, models, quota)
|
|
34
35
|
sisu help
|
|
35
36
|
|
|
36
37
|
Auth and workspaces live in $SISU_HOME (default ~/.sisu), shared with Desktop.
|
|
@@ -51,8 +52,8 @@ function parseArgs(args) {
|
|
|
51
52
|
const switches = new Set();
|
|
52
53
|
for (let i = 0; i < args.length; i += 1) {
|
|
53
54
|
const item = args[i];
|
|
54
|
-
if (item === '--new') {
|
|
55
|
-
switches.add('
|
|
55
|
+
if (item === '--new' || item === '--stub' || item === '-p' || item === '--print') {
|
|
56
|
+
switches.add(item.replace(/^-+/, ''));
|
|
56
57
|
continue;
|
|
57
58
|
}
|
|
58
59
|
if (item.startsWith('--')) {
|
|
@@ -123,7 +124,7 @@ async function runCli(argv, deps = {}) {
|
|
|
123
124
|
process.stdout.write(`${(0, commands_1.listLocalCommand)(flag(args, '--project'))}\n`);
|
|
124
125
|
return 0;
|
|
125
126
|
}
|
|
126
|
-
if (command === 'exec') {
|
|
127
|
+
if (command === 'exec' || command === '-p' || command === '--print') {
|
|
127
128
|
const parsed = parseArgs(args);
|
|
128
129
|
const prompt = parsed.rest.join(' ').trim();
|
|
129
130
|
if (!prompt) {
|
|
@@ -134,6 +135,7 @@ async function runCli(argv, deps = {}) {
|
|
|
134
135
|
projectId: parsed.flags['--project'],
|
|
135
136
|
model: parsed.flags['--model'],
|
|
136
137
|
newConversation: parsed.switches.has('new'),
|
|
138
|
+
stub: parsed.switches.has('stub') || process.env.SISU_RUNTIME_STUB === '1',
|
|
137
139
|
});
|
|
138
140
|
if (result.text)
|
|
139
141
|
process.stdout.write(`${result.text}\n`);
|
package/dist/pager/app.js
CHANGED
|
@@ -28,9 +28,9 @@ function formatChromeStatus(email, quota, conversationId = '', model = '') {
|
|
|
28
28
|
const conv = shortConversationId(conversationId);
|
|
29
29
|
const modelName = (model || '').trim();
|
|
30
30
|
if (!who) {
|
|
31
|
-
return conv ? `sisu · not signed in · ${conv}` : 'sisu · not signed in';
|
|
31
|
+
return conv ? `sisu local · not signed in · ${conv}` : 'sisu local · not signed in';
|
|
32
32
|
}
|
|
33
|
-
const parts = [who];
|
|
33
|
+
const parts = [who, 'local'];
|
|
34
34
|
if (modelName)
|
|
35
35
|
parts.push(modelName);
|
|
36
36
|
const quotaText = (quota || '').trim();
|
|
@@ -63,7 +63,7 @@ function pushEntry(state, kind, text) {
|
|
|
63
63
|
return { ...state, entries, selected: entries.length - 1 };
|
|
64
64
|
}
|
|
65
65
|
function clearDraft(state) {
|
|
66
|
-
return { ...state, draft: '', slashOpen: false, slashIndex: 0 };
|
|
66
|
+
return { ...state, draft: '', slashOpen: false, slashIndex: 0, draftIndex: 0, historyIndex: -1, stashDraft: '' };
|
|
67
67
|
}
|
|
68
68
|
function submittedText(state) {
|
|
69
69
|
const raw = state.draft.trim();
|
package/dist/pager/input.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.decodeKeys = decodeKeys;
|
|
4
4
|
/**
|
|
5
5
|
* Decode a raw terminal input chunk into pager keys.
|
|
6
|
-
* Incomplete CSI sequences are returned in `rest` so the next read can finish them.
|
|
6
|
+
* Incomplete CSI/SS3 sequences are returned in `rest` so the next read can finish them.
|
|
7
7
|
* `\x03` (Ctrl+C) maps to escape; the app treats escape at empty draft as quit.
|
|
8
8
|
*/
|
|
9
9
|
function decodeKeys(chunk) {
|
|
@@ -12,37 +12,52 @@ function decodeKeys(chunk) {
|
|
|
12
12
|
while (i < chunk.length) {
|
|
13
13
|
const ch = chunk[i];
|
|
14
14
|
if (ch === '\x1b') {
|
|
15
|
-
// Incomplete CSI prefix — hold for the next chunk.
|
|
16
15
|
if (i + 1 === chunk.length) {
|
|
17
16
|
keys.push({ type: 'escape' });
|
|
18
17
|
i += 1;
|
|
19
18
|
continue;
|
|
20
19
|
}
|
|
21
|
-
if (chunk[i + 1] === '
|
|
22
|
-
if (i + 2 >= chunk.length)
|
|
20
|
+
if (chunk[i + 1] === 'O') {
|
|
21
|
+
if (i + 2 >= chunk.length)
|
|
23
22
|
return { keys, rest: chunk.slice(i) };
|
|
24
|
-
|
|
25
|
-
const code = chunk[i + 2];
|
|
26
|
-
const arrow = code === 'A'
|
|
27
|
-
? 'up'
|
|
28
|
-
: code === 'B'
|
|
29
|
-
? 'down'
|
|
30
|
-
: code === 'C'
|
|
31
|
-
? 'right'
|
|
32
|
-
: code === 'D'
|
|
33
|
-
? 'left'
|
|
34
|
-
: null;
|
|
23
|
+
const arrow = arrowFrom(chunk[i + 2]);
|
|
35
24
|
if (arrow) {
|
|
36
25
|
keys.push({ type: arrow });
|
|
37
26
|
i += 3;
|
|
38
27
|
continue;
|
|
39
28
|
}
|
|
40
|
-
// Unknown completed CSI: treat ESC as escape and rescan from '['.
|
|
41
29
|
keys.push({ type: 'escape' });
|
|
42
30
|
i += 1;
|
|
43
31
|
continue;
|
|
44
32
|
}
|
|
45
|
-
|
|
33
|
+
if (chunk[i + 1] === '[') {
|
|
34
|
+
let j = i + 2;
|
|
35
|
+
while (j < chunk.length && /[0-9;]/.test(chunk[j]))
|
|
36
|
+
j += 1;
|
|
37
|
+
if (j >= chunk.length)
|
|
38
|
+
return { keys, rest: chunk.slice(i) };
|
|
39
|
+
const body = chunk.slice(i + 2, j);
|
|
40
|
+
const term = chunk[j];
|
|
41
|
+
const arrow = arrowFrom(term);
|
|
42
|
+
if (arrow) {
|
|
43
|
+
keys.push({ type: arrow });
|
|
44
|
+
i = j + 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (term === '~' && body === '5') {
|
|
48
|
+
keys.push({ type: 'pageup' });
|
|
49
|
+
i = j + 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (term === '~' && body === '6') {
|
|
53
|
+
keys.push({ type: 'pagedown' });
|
|
54
|
+
i = j + 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
keys.push({ type: 'escape' });
|
|
58
|
+
i += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
46
61
|
keys.push({ type: 'escape' });
|
|
47
62
|
i += 1;
|
|
48
63
|
continue;
|
|
@@ -57,19 +72,16 @@ function decodeKeys(chunk) {
|
|
|
57
72
|
i += 1;
|
|
58
73
|
continue;
|
|
59
74
|
}
|
|
60
|
-
// Ctrl+C → escape (app treats escape at empty draft as quit)
|
|
61
75
|
if (ch === '\x03') {
|
|
62
76
|
keys.push({ type: 'escape' });
|
|
63
77
|
i += 1;
|
|
64
78
|
continue;
|
|
65
79
|
}
|
|
66
|
-
// Skip other C0 controls (except those handled above)
|
|
67
80
|
const code = ch.charCodeAt(0);
|
|
68
81
|
if (code < 0x20 || code === 0x7f) {
|
|
69
82
|
i += 1;
|
|
70
83
|
continue;
|
|
71
84
|
}
|
|
72
|
-
// Printable UTF-16 code unit / surrogate pair as one char
|
|
73
85
|
const cp = chunk.codePointAt(i);
|
|
74
86
|
if (cp === undefined) {
|
|
75
87
|
i += 1;
|
|
@@ -81,3 +93,14 @@ function decodeKeys(chunk) {
|
|
|
81
93
|
}
|
|
82
94
|
return { keys, rest: '' };
|
|
83
95
|
}
|
|
96
|
+
function arrowFrom(code) {
|
|
97
|
+
if (code === 'A')
|
|
98
|
+
return 'up';
|
|
99
|
+
if (code === 'B')
|
|
100
|
+
return 'down';
|
|
101
|
+
if (code === 'C')
|
|
102
|
+
return 'right';
|
|
103
|
+
if (code === 'D')
|
|
104
|
+
return 'left';
|
|
105
|
+
return null;
|
|
106
|
+
}
|
package/dist/pager/model.js
CHANGED
|
@@ -44,6 +44,9 @@ function createPagerState() {
|
|
|
44
44
|
slashOpen: false,
|
|
45
45
|
conversationId: '',
|
|
46
46
|
slashIndex: 0,
|
|
47
|
+
draftIndex: 0,
|
|
48
|
+
historyIndex: -1,
|
|
49
|
+
stashDraft: '',
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
function clampSelected(entries, selected) {
|
|
@@ -65,11 +68,40 @@ function clampSlashIndex(draft, slashIndex) {
|
|
|
65
68
|
return 0;
|
|
66
69
|
return slashIndex;
|
|
67
70
|
}
|
|
68
|
-
function
|
|
71
|
+
function draftChars(draft) {
|
|
72
|
+
return Array.from(draft);
|
|
73
|
+
}
|
|
74
|
+
function clampDraftIndex(draft, index) {
|
|
75
|
+
const n = draftChars(draft).length;
|
|
76
|
+
if (index < 0)
|
|
77
|
+
return 0;
|
|
78
|
+
if (index > n)
|
|
79
|
+
return n;
|
|
80
|
+
return index;
|
|
81
|
+
}
|
|
82
|
+
function insertAt(draft, index, value) {
|
|
83
|
+
const chars = draftChars(draft);
|
|
84
|
+
const at = clampDraftIndex(draft, index);
|
|
85
|
+
chars.splice(at, 0, ...Array.from(value));
|
|
86
|
+
return { draft: chars.join(''), draftIndex: at + Array.from(value).length };
|
|
87
|
+
}
|
|
88
|
+
function deleteBefore(draft, index) {
|
|
89
|
+
const chars = draftChars(draft);
|
|
90
|
+
const at = clampDraftIndex(draft, index);
|
|
91
|
+
if (at <= 0)
|
|
92
|
+
return { draft, draftIndex: 0 };
|
|
93
|
+
chars.splice(at - 1, 1);
|
|
94
|
+
return { draft: chars.join(''), draftIndex: at - 1 };
|
|
95
|
+
}
|
|
96
|
+
function userPrompts(state) {
|
|
97
|
+
return state.entries.filter((entry) => entry.kind === 'user').map((entry) => entry.text);
|
|
98
|
+
}
|
|
99
|
+
function withDraft(state, draft, slashOpen, draftIndex) {
|
|
69
100
|
const open = slashOpen ?? (draft.startsWith('/') ? state.slashOpen || draft === '/' : false);
|
|
70
101
|
return {
|
|
71
102
|
...state,
|
|
72
103
|
draft,
|
|
104
|
+
draftIndex: clampDraftIndex(draft, draftIndex ?? draftChars(draft).length),
|
|
73
105
|
slashOpen: open && draft.startsWith('/'),
|
|
74
106
|
slashIndex: open && draft.startsWith('/') ? clampSlashIndex(draft, state.slashIndex) : 0,
|
|
75
107
|
};
|
|
@@ -146,29 +178,57 @@ function appendText(state, text) {
|
|
|
146
178
|
const seeded = startAssistant(state);
|
|
147
179
|
return appendText(seeded, text);
|
|
148
180
|
}
|
|
181
|
+
function applyHistory(state, delta) {
|
|
182
|
+
const prompts = userPrompts(state);
|
|
183
|
+
if (prompts.length === 0) {
|
|
184
|
+
if (state.draft === '' && state.entries.length > 0) {
|
|
185
|
+
const selected = clampSelected(state.entries, state.selected + delta);
|
|
186
|
+
return { ...state, selected };
|
|
187
|
+
}
|
|
188
|
+
return state;
|
|
189
|
+
}
|
|
190
|
+
let historyIndex = state.historyIndex;
|
|
191
|
+
let stashDraft = state.stashDraft;
|
|
192
|
+
if (historyIndex < 0) {
|
|
193
|
+
if (delta > 0)
|
|
194
|
+
return state;
|
|
195
|
+
stashDraft = state.draft;
|
|
196
|
+
historyIndex = prompts.length - 1;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
historyIndex += delta;
|
|
200
|
+
}
|
|
201
|
+
if (historyIndex < 0)
|
|
202
|
+
historyIndex = 0;
|
|
203
|
+
if (historyIndex >= prompts.length) {
|
|
204
|
+
return withDraft({ ...state, historyIndex: -1, stashDraft: '' }, stashDraft, stashDraft.startsWith('/'));
|
|
205
|
+
}
|
|
206
|
+
const draft = prompts[historyIndex];
|
|
207
|
+
return withDraft({ ...state, historyIndex, stashDraft }, draft, draft.startsWith('/'));
|
|
208
|
+
}
|
|
149
209
|
function applyKey(state, key) {
|
|
150
210
|
switch (key.type) {
|
|
151
211
|
case 'char': {
|
|
152
|
-
const
|
|
212
|
+
const next = insertAt(state.draft, state.draftIndex, key.value);
|
|
153
213
|
if (key.value === '/' && state.draft === '') {
|
|
154
|
-
return { ...state, draft: '/', slashOpen: true, slashIndex: 0 };
|
|
214
|
+
return { ...state, draft: '/', draftIndex: 1, slashOpen: true, slashIndex: 0, historyIndex: -1 };
|
|
155
215
|
}
|
|
156
|
-
if (state.slashOpen || draft.startsWith('/')) {
|
|
157
|
-
return withDraft(state, draft, draft.startsWith('/'));
|
|
216
|
+
if (state.slashOpen || next.draft.startsWith('/')) {
|
|
217
|
+
return withDraft({ ...state, historyIndex: -1 }, next.draft, next.draft.startsWith('/'), next.draftIndex);
|
|
158
218
|
}
|
|
159
|
-
return { ...state, draft };
|
|
219
|
+
return { ...state, draft: next.draft, draftIndex: next.draftIndex, historyIndex: -1 };
|
|
160
220
|
}
|
|
161
221
|
case 'backspace': {
|
|
162
222
|
if (!state.draft)
|
|
163
223
|
return state;
|
|
164
|
-
const
|
|
224
|
+
const next = deleteBefore(state.draft, state.draftIndex);
|
|
165
225
|
if (state.slashOpen) {
|
|
166
|
-
if (!draft.startsWith('/')) {
|
|
167
|
-
return { ...state, draft, slashOpen: false, slashIndex: 0 };
|
|
226
|
+
if (!next.draft.startsWith('/')) {
|
|
227
|
+
return { ...state, draft: next.draft, draftIndex: next.draftIndex, slashOpen: false, slashIndex: 0 };
|
|
168
228
|
}
|
|
169
|
-
return withDraft(state, draft, true);
|
|
229
|
+
return withDraft(state, next.draft, true, next.draftIndex);
|
|
170
230
|
}
|
|
171
|
-
return { ...state, draft };
|
|
231
|
+
return { ...state, draft: next.draft, draftIndex: next.draftIndex };
|
|
172
232
|
}
|
|
173
233
|
case 'escape': {
|
|
174
234
|
if (state.slashOpen) {
|
|
@@ -177,13 +237,20 @@ function applyKey(state, key) {
|
|
|
177
237
|
return state;
|
|
178
238
|
}
|
|
179
239
|
case 'enter': {
|
|
180
|
-
|
|
181
|
-
return state;
|
|
240
|
+
return { ...state, historyIndex: -1, stashDraft: '' };
|
|
182
241
|
}
|
|
183
|
-
case 'left':
|
|
242
|
+
case 'left': {
|
|
243
|
+
if (state.draft) {
|
|
244
|
+
return { ...state, draftIndex: clampDraftIndex(state.draft, state.draftIndex - 1) };
|
|
245
|
+
}
|
|
184
246
|
return setEntryFold(state, true);
|
|
185
|
-
|
|
247
|
+
}
|
|
248
|
+
case 'right': {
|
|
249
|
+
if (state.draft) {
|
|
250
|
+
return { ...state, draftIndex: clampDraftIndex(state.draft, state.draftIndex + 1) };
|
|
251
|
+
}
|
|
186
252
|
return setEntryFold(state, false);
|
|
253
|
+
}
|
|
187
254
|
case 'up': {
|
|
188
255
|
if (state.slashOpen) {
|
|
189
256
|
const items = filterSlash(state.draft);
|
|
@@ -192,11 +259,7 @@ function applyKey(state, key) {
|
|
|
192
259
|
const slashIndex = (state.slashIndex - 1 + items.length) % items.length;
|
|
193
260
|
return { ...state, slashIndex };
|
|
194
261
|
}
|
|
195
|
-
|
|
196
|
-
const selected = clampSelected(state.entries, state.selected - 1);
|
|
197
|
-
return { ...state, selected };
|
|
198
|
-
}
|
|
199
|
-
return state;
|
|
262
|
+
return applyHistory(state, -1);
|
|
200
263
|
}
|
|
201
264
|
case 'down': {
|
|
202
265
|
if (state.slashOpen) {
|
|
@@ -206,11 +269,17 @@ function applyKey(state, key) {
|
|
|
206
269
|
const slashIndex = (state.slashIndex + 1) % items.length;
|
|
207
270
|
return { ...state, slashIndex };
|
|
208
271
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
272
|
+
return applyHistory(state, 1);
|
|
273
|
+
}
|
|
274
|
+
case 'pageup': {
|
|
275
|
+
if (state.entries.length === 0)
|
|
276
|
+
return state;
|
|
277
|
+
return { ...state, selected: clampSelected(state.entries, state.selected - 8) };
|
|
278
|
+
}
|
|
279
|
+
case 'pagedown': {
|
|
280
|
+
if (state.entries.length === 0)
|
|
281
|
+
return state;
|
|
282
|
+
return { ...state, selected: clampSelected(state.entries, state.selected + 8) };
|
|
214
283
|
}
|
|
215
284
|
default:
|
|
216
285
|
return state;
|