koneck 2.62.0 → 2.64.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/dist/folder-dialog.d.ts +63 -0
- package/dist/folder-dialog.d.ts.map +1 -0
- package/dist/folder-dialog.js +175 -0
- package/dist/folder-dialog.js.map +1 -0
- package/dist/session-title.d.ts +70 -0
- package/dist/session-title.d.ts.map +1 -0
- package/dist/session-title.js +167 -0
- package/dist/session-title.js.map +1 -0
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +74 -4
- package/dist/web/api.js.map +1 -1
- package/dist/web/sessions.d.ts +4 -0
- package/dist/web/sessions.d.ts.map +1 -1
- package/dist/web/sessions.js +64 -1
- package/dist/web/sessions.js.map +1 -1
- package/dist/web/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +92 -8
- package/dist/web/ui-client.js.map +1 -1
- package/dist/web/ui-css.d.ts.map +1 -1
- package/dist/web/ui-css.js +15 -0
- package/dist/web/ui-css.js.map +1 -1
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +43 -10
- package/dist/web/ui.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The computer's own folder picker, opened by the server.
|
|
3
|
+
*
|
|
4
|
+
* A browser cannot do this, and it is worth being precise about why rather than treating it as a
|
|
5
|
+
* missing feature. `<input type="file" webkitdirectory>` hands back file entries with names relative
|
|
6
|
+
* to the chosen folder and no absolute path; `showDirectoryPicker()` hands back a handle and no
|
|
7
|
+
* path either. Both withhold it deliberately — a page that learned where you keep your files has
|
|
8
|
+
* learned something about you that it was not given.
|
|
9
|
+
*
|
|
10
|
+
* But KONECK's server is on the same machine as the files, and acting through it already requires
|
|
11
|
+
* loopback and the token. So the *server* opens the dialog: the real one, the one every other
|
|
12
|
+
* application on the machine uses, and the path comes back because the process asking is entitled
|
|
13
|
+
* to know it.
|
|
14
|
+
*
|
|
15
|
+
* Two honest limits, both reported rather than papered over. The dialog appears on the machine
|
|
16
|
+
* running KONECK — which is the same machine, because acting is loopback-only, but somebody
|
|
17
|
+
* tunnelling a port should know that. And a machine with no desktop session has no dialog to show:
|
|
18
|
+
* over SSH there is nothing to put on screen, so the answer is to say so and let the built-in
|
|
19
|
+
* browser do the job instead.
|
|
20
|
+
*/
|
|
21
|
+
/** What came back, or why nothing did. */
|
|
22
|
+
export type FolderChoice = {
|
|
23
|
+
ok: true;
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
/** The person closed the dialog. Not an error, and must not be reported as one. */
|
|
27
|
+
| {
|
|
28
|
+
ok: false;
|
|
29
|
+
cancelled: true;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
cancelled: false;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Runs a dialog and stops waiting if it stops answering.
|
|
37
|
+
*
|
|
38
|
+
* execa's own timeout signals the process it started and then waits for its pipes to close, which
|
|
39
|
+
* is not the same thing — a dialog that dies in a way execa does not notice left the request open
|
|
40
|
+
* for the full two minutes. Measured: a dialog killed from outside took 120,027ms to settle, while
|
|
41
|
+
* one that exited cleanly took 2,588ms. So the wait is raced against a deadline and the process
|
|
42
|
+
* group is signalled, which is the same shape as the fix the command runner already needed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runDialog(file: string, args: readonly string[],
|
|
45
|
+
/** Injectable so the giving-up path can be tested without waiting two minutes for it. */
|
|
46
|
+
waitMs?: number): Promise<{
|
|
47
|
+
code: number | undefined;
|
|
48
|
+
out: string;
|
|
49
|
+
err: string;
|
|
50
|
+
gaveUp: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
/** Whether a desktop session exists to show a dialog on. */
|
|
53
|
+
export declare function hasDesktop(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
54
|
+
/** Which dialog tool would be used, or null when none is installed. */
|
|
55
|
+
export declare function dialogAvailable(platform?: NodeJS.Platform): Promise<string | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Opens the folder dialog and waits for an answer.
|
|
58
|
+
*
|
|
59
|
+
* A generous timeout, because the answer is a person deciding: two minutes is long enough to think
|
|
60
|
+
* and short enough that a dialog nobody is looking at does not hold a handle for ever.
|
|
61
|
+
*/
|
|
62
|
+
export declare function chooseFolder(startIn?: string, title?: string, platform?: NodeJS.Platform): Promise<FolderChoice>;
|
|
63
|
+
//# sourceMappingURL=folder-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-dialog.d.ts","sourceRoot":"","sources":["../src/folder-dialog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,0CAA0C;AAC1C,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC5B,mFAAmF;GACjF;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyBpD;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE;AACrC,yFAAyF;AACzF,MAAM,SAAiB,GACtB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CAyBlF;AAKD,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,QAAQ,kBAAmB,GAAG,OAAO,CAGrG;AAED,uEAAuE;AACvE,wBAAsB,eAAe,CACnC,QAAQ,kBAAmB,GAC1B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,OAAO,SAAe,EACtB,KAAK,SAAmB,EACxB,QAAQ,kBAAmB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA8CvB"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The computer's own folder picker, opened by the server.
|
|
3
|
+
*
|
|
4
|
+
* A browser cannot do this, and it is worth being precise about why rather than treating it as a
|
|
5
|
+
* missing feature. `<input type="file" webkitdirectory>` hands back file entries with names relative
|
|
6
|
+
* to the chosen folder and no absolute path; `showDirectoryPicker()` hands back a handle and no
|
|
7
|
+
* path either. Both withhold it deliberately — a page that learned where you keep your files has
|
|
8
|
+
* learned something about you that it was not given.
|
|
9
|
+
*
|
|
10
|
+
* But KONECK's server is on the same machine as the files, and acting through it already requires
|
|
11
|
+
* loopback and the token. So the *server* opens the dialog: the real one, the one every other
|
|
12
|
+
* application on the machine uses, and the path comes back because the process asking is entitled
|
|
13
|
+
* to know it.
|
|
14
|
+
*
|
|
15
|
+
* Two honest limits, both reported rather than papered over. The dialog appears on the machine
|
|
16
|
+
* running KONECK — which is the same machine, because acting is loopback-only, but somebody
|
|
17
|
+
* tunnelling a port should know that. And a machine with no desktop session has no dialog to show:
|
|
18
|
+
* over SSH there is nothing to put on screen, so the answer is to say so and let the built-in
|
|
19
|
+
* browser do the job instead.
|
|
20
|
+
*/
|
|
21
|
+
import os from 'os';
|
|
22
|
+
import { existsSync } from 'fs';
|
|
23
|
+
/**
|
|
24
|
+
* The ways to ask, in the order worth trying.
|
|
25
|
+
*
|
|
26
|
+
* zenity and kdialog are the GTK and KDE dialogs, so one of them is present on most desktops; yad
|
|
27
|
+
* is a fork of zenity that some distributions ship instead. macOS has AppleScript, which drives the
|
|
28
|
+
* real Finder dialog. Windows has PowerShell, whose folder browser is the shell's own.
|
|
29
|
+
*/
|
|
30
|
+
const LINUX_DIALOGS = [
|
|
31
|
+
{ file: 'zenity', args: (title, startIn) => ['--file-selection', '--directory', `--title=${title}`, `--filename=${startIn}/`] },
|
|
32
|
+
{ file: 'kdialog', args: (title, startIn) => ['--getexistingdirectory', startIn, '--title', title] },
|
|
33
|
+
{ file: 'qarma', args: (title, startIn) => ['--file-selection', '--directory', `--title=${title}`, `--filename=${startIn}/`] },
|
|
34
|
+
{ file: 'yad', args: (title, startIn) => ['--file-selection', '--directory', `--title=${title}`, `--filename=${startIn}/`] },
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Runs a dialog and stops waiting if it stops answering.
|
|
38
|
+
*
|
|
39
|
+
* execa's own timeout signals the process it started and then waits for its pipes to close, which
|
|
40
|
+
* is not the same thing — a dialog that dies in a way execa does not notice left the request open
|
|
41
|
+
* for the full two minutes. Measured: a dialog killed from outside took 120,027ms to settle, while
|
|
42
|
+
* one that exited cleanly took 2,588ms. So the wait is raced against a deadline and the process
|
|
43
|
+
* group is signalled, which is the same shape as the fix the command runner already needed.
|
|
44
|
+
*/
|
|
45
|
+
export async function runDialog(file, args,
|
|
46
|
+
/** Injectable so the giving-up path can be tested without waiting two minutes for it. */
|
|
47
|
+
waitMs = DIALOG_WAIT_MS) {
|
|
48
|
+
const { execa } = await import('execa');
|
|
49
|
+
const child = execa(file, [...args], { reject: false, detached: true });
|
|
50
|
+
const GAVE_UP = Symbol('gave-up');
|
|
51
|
+
let timer;
|
|
52
|
+
const deadline = new Promise(resolve => {
|
|
53
|
+
timer = setTimeout(() => resolve(GAVE_UP), waitMs);
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const settled = await Promise.race([child.then(r => r), deadline]);
|
|
57
|
+
if (settled === GAVE_UP) {
|
|
58
|
+
// The whole group, because the dialog may have re-executed itself into a child.
|
|
59
|
+
try {
|
|
60
|
+
if (child.pid)
|
|
61
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
try {
|
|
65
|
+
child.kill('SIGKILL');
|
|
66
|
+
}
|
|
67
|
+
catch { /* already gone */ }
|
|
68
|
+
}
|
|
69
|
+
return { code: undefined, out: '', err: '', gaveUp: true };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
code: settled.exitCode ?? undefined,
|
|
73
|
+
out: String(settled.stdout ?? ''),
|
|
74
|
+
err: String(settled.stderr ?? ''),
|
|
75
|
+
gaveUp: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (timer)
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** How long a dialog nobody is answering may hold a request open. */
|
|
84
|
+
const DIALOG_WAIT_MS = 120_000;
|
|
85
|
+
/** Whether a desktop session exists to show a dialog on. */
|
|
86
|
+
export function hasDesktop(env = process.env, platform = process.platform) {
|
|
87
|
+
if (platform === 'darwin' || platform === 'win32')
|
|
88
|
+
return true;
|
|
89
|
+
return !!(env['DISPLAY'] || env['WAYLAND_DISPLAY']);
|
|
90
|
+
}
|
|
91
|
+
/** Which dialog tool would be used, or null when none is installed. */
|
|
92
|
+
export async function dialogAvailable(platform = process.platform) {
|
|
93
|
+
if (platform === 'darwin')
|
|
94
|
+
return 'osascript';
|
|
95
|
+
if (platform === 'win32')
|
|
96
|
+
return 'powershell';
|
|
97
|
+
const { execa } = await import('execa');
|
|
98
|
+
for (const dialog of LINUX_DIALOGS) {
|
|
99
|
+
const found = await execa('command', ['-v', dialog.file], { shell: true, reject: false });
|
|
100
|
+
if (found.exitCode === 0 && String(found.stdout).trim() !== '')
|
|
101
|
+
return dialog.file;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Opens the folder dialog and waits for an answer.
|
|
107
|
+
*
|
|
108
|
+
* A generous timeout, because the answer is a person deciding: two minutes is long enough to think
|
|
109
|
+
* and short enough that a dialog nobody is looking at does not hold a handle for ever.
|
|
110
|
+
*/
|
|
111
|
+
export async function chooseFolder(startIn = os.homedir(), title = 'Open a project', platform = process.platform) {
|
|
112
|
+
if (!hasDesktop()) {
|
|
113
|
+
return { ok: false, cancelled: false, reason: 'This machine has no desktop session, so there is no dialog to show — KONECK is probably '
|
|
114
|
+
+ 'running over SSH. Use the folder list instead.' };
|
|
115
|
+
}
|
|
116
|
+
const from = existsSync(startIn) ? startIn : os.homedir();
|
|
117
|
+
if (platform === 'darwin') {
|
|
118
|
+
// AppleScript drives the real Finder dialog, and returns POSIX path text.
|
|
119
|
+
const script = `set chosen to choose folder with prompt ${JSON.stringify(title)} `
|
|
120
|
+
+ `default location POSIX file ${JSON.stringify(from)}\n`
|
|
121
|
+
+ 'return POSIX path of chosen';
|
|
122
|
+
const done = await runDialog('osascript', ['-e', script]);
|
|
123
|
+
if (done.gaveUp)
|
|
124
|
+
return gaveUp();
|
|
125
|
+
if (done.code !== 0)
|
|
126
|
+
return cancelledOrFailed(done.err, done.code);
|
|
127
|
+
return finish(done.out);
|
|
128
|
+
}
|
|
129
|
+
if (platform === 'win32') {
|
|
130
|
+
const script = 'Add-Type -AssemblyName System.Windows.Forms; '
|
|
131
|
+
+ '$d = New-Object System.Windows.Forms.FolderBrowserDialog; '
|
|
132
|
+
+ `$d.Description = ${JSON.stringify(title)}; `
|
|
133
|
+
+ `$d.SelectedPath = ${JSON.stringify(from)}; `
|
|
134
|
+
+ 'if ($d.ShowDialog() -eq "OK") { $d.SelectedPath } else { exit 1 }';
|
|
135
|
+
const done = await runDialog('powershell.exe', ['-NoProfile', '-Command', script]);
|
|
136
|
+
if (done.gaveUp)
|
|
137
|
+
return gaveUp();
|
|
138
|
+
if (done.code !== 0)
|
|
139
|
+
return cancelledOrFailed(done.err, done.code);
|
|
140
|
+
return finish(done.out);
|
|
141
|
+
}
|
|
142
|
+
const tool = await dialogAvailable(platform);
|
|
143
|
+
if (!tool) {
|
|
144
|
+
return { ok: false, cancelled: false, reason: 'No folder dialog is installed. On most desktops that is the zenity package (or kdialog on '
|
|
145
|
+
+ 'KDE). Use the folder list instead.' };
|
|
146
|
+
}
|
|
147
|
+
const dialog = LINUX_DIALOGS.find(d => d.file === tool);
|
|
148
|
+
const done = await runDialog(dialog.file, dialog.args(title, from));
|
|
149
|
+
if (done.gaveUp)
|
|
150
|
+
return gaveUp();
|
|
151
|
+
// Every one of these exits non-zero when the dialog is dismissed, which is a choice not to choose
|
|
152
|
+
// rather than a failure, and must not be reported as one.
|
|
153
|
+
if (done.code !== 0)
|
|
154
|
+
return cancelledOrFailed(done.err, done.code);
|
|
155
|
+
return finish(done.out);
|
|
156
|
+
}
|
|
157
|
+
function gaveUp() {
|
|
158
|
+
return { ok: false, cancelled: false, reason: 'the folder dialog stopped answering and was closed — try the folder list instead' };
|
|
159
|
+
}
|
|
160
|
+
function cancelledOrFailed(stderr, code) {
|
|
161
|
+
const said = String(stderr ?? '').trim();
|
|
162
|
+
// A dismissed dialog says nothing; a broken one says something.
|
|
163
|
+
if (said === '')
|
|
164
|
+
return { ok: false, cancelled: true };
|
|
165
|
+
return { ok: false, cancelled: false,
|
|
166
|
+
reason: `the folder dialog failed${code !== undefined ? ` (${code})` : ''}: ${said}` };
|
|
167
|
+
}
|
|
168
|
+
function finish(stdout) {
|
|
169
|
+
// zenity can return several paths separated by | when multiple selection is on; take the first.
|
|
170
|
+
const path = String(stdout ?? '').split('|')[0].trim().replace(/\/+$/, '');
|
|
171
|
+
if (path === '')
|
|
172
|
+
return { ok: false, cancelled: true };
|
|
173
|
+
return { ok: true, path };
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=folder-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-dialog.js","sourceRoot":"","sources":["../src/folder-dialog.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a session is called.
|
|
3
|
+
*
|
|
4
|
+
* It was called whatever you typed first, truncated to eighty characters, for ever. So a session
|
|
5
|
+
* that began "hi" and went on to build a ledger page was called "hi" — and the picker, which exists
|
|
6
|
+
* to answer "which of these was the one about X", answered "hi", "ok", "continue" and "what is 2+2"
|
|
7
|
+
* for a week's work.
|
|
8
|
+
*
|
|
9
|
+
* Two mistakes in one line of code. The first message is often not the request: it is a greeting, an
|
|
10
|
+
* acknowledgement, a "carry on". And a title set once cannot follow a session that turned into
|
|
11
|
+
* something else, which is what sessions do.
|
|
12
|
+
*
|
|
13
|
+
* So the name is derived from the substance and revised as the substance changes. Mechanically
|
|
14
|
+
* first, because that always works and costs nothing: the earliest message that actually asks for
|
|
15
|
+
* something, cleaned up. Then, once there is enough to summarise, the model is asked for a short
|
|
16
|
+
* one — which is the only way to get "Stokvel dashboard with recharts" out of a conversation that
|
|
17
|
+
* happens to open with "hi". That ask is cheap, happens once, and never blocks a turn; when it
|
|
18
|
+
* fails the mechanical name stands.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Messages, as much of them as naming needs.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately loose. The provider's own message type has a narrower role union and several content
|
|
24
|
+
* shapes, some of which omit content altogether, and a stricter shape here would refuse the very
|
|
25
|
+
* array it exists to read. Naming only needs to know who said something and what it said.
|
|
26
|
+
*/
|
|
27
|
+
export interface Said {
|
|
28
|
+
role?: unknown;
|
|
29
|
+
content?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export declare function isPleasantry(text: string): boolean;
|
|
32
|
+
/** The text of a message, whatever shape its content came in. */
|
|
33
|
+
export declare function textOf(message: Said | undefined): string;
|
|
34
|
+
/**
|
|
35
|
+
* A name from the conversation itself, with no model involved.
|
|
36
|
+
*
|
|
37
|
+
* The earliest thing said that asks for something: greetings are skipped, KONECK's own prefixes are
|
|
38
|
+
* skipped, and a message too short to mean anything is skipped. Falls back to the first message
|
|
39
|
+
* there is — because a session called "hi" is still better than one called nothing — and to a plain
|
|
40
|
+
* label when even that is missing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mechanicalTitle(messages: readonly Said[], limit?: number): string;
|
|
43
|
+
/** One line, no markdown fences, no newlines, trimmed to length at a word boundary. */
|
|
44
|
+
export declare function tidy(text: string, limit?: number): string;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a session has enough in it to be worth naming properly.
|
|
47
|
+
*
|
|
48
|
+
* Asking a model to name "hi" wastes a call and produces a title about greetings. Two exchanges, or
|
|
49
|
+
* one substantial request, is the point at which there is something to summarise.
|
|
50
|
+
*/
|
|
51
|
+
export declare function worthNaming(messages: readonly Said[]): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* What to ask the model for.
|
|
54
|
+
*
|
|
55
|
+
* The instruction is narrow on purpose: a model asked to "summarise" writes a sentence, and a model
|
|
56
|
+
* asked for a title writes "Session Summary: Implementation of...". Naming the shape wanted, giving
|
|
57
|
+
* an example of the register, and forbidding the two habits it will otherwise reach for is what gets
|
|
58
|
+
* a usable answer back in one attempt rather than three.
|
|
59
|
+
*/
|
|
60
|
+
export declare function titleQuestion(messages: readonly Said[], take?: number): string;
|
|
61
|
+
/**
|
|
62
|
+
* The model's answer, made safe to use as a title, or null when it is not usable.
|
|
63
|
+
*
|
|
64
|
+
* A model asked for six words will sometimes send a paragraph, a quoted phrase, a bulleted list or
|
|
65
|
+
* an apology. Anything that does not look like a label is refused, and the mechanical name stands —
|
|
66
|
+
* a bad title is worse than a plain one, because a picker full of "Certainly! Here is a title" is
|
|
67
|
+
* unreadable in a way that "hi" at least is not.
|
|
68
|
+
*/
|
|
69
|
+
export declare function usableTitle(answer: string, limit?: number): string | null;
|
|
70
|
+
//# sourceMappingURL=session-title.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-title.d.ts","sourceRoot":"","sources":["../src/session-title.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA2BD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,iEAAiE;AACjE,wBAAgB,MAAM,CAAC,OAAO,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,CAWxD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,IAAI,EAAE,EAAE,KAAK,SAAK,GAAG,MAAM,CAW7E;AAED,uFAAuF;AACvF,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,CAUrD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,SAAS,IAAI,EAAE,GAAG,OAAO,CAM9D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,IAAI,EAAE,EAAE,IAAI,SAAI,GAAG,MAAM,CAazE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,GAAG,IAAI,CA4BrE"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a session is called.
|
|
3
|
+
*
|
|
4
|
+
* It was called whatever you typed first, truncated to eighty characters, for ever. So a session
|
|
5
|
+
* that began "hi" and went on to build a ledger page was called "hi" — and the picker, which exists
|
|
6
|
+
* to answer "which of these was the one about X", answered "hi", "ok", "continue" and "what is 2+2"
|
|
7
|
+
* for a week's work.
|
|
8
|
+
*
|
|
9
|
+
* Two mistakes in one line of code. The first message is often not the request: it is a greeting, an
|
|
10
|
+
* acknowledgement, a "carry on". And a title set once cannot follow a session that turned into
|
|
11
|
+
* something else, which is what sessions do.
|
|
12
|
+
*
|
|
13
|
+
* So the name is derived from the substance and revised as the substance changes. Mechanically
|
|
14
|
+
* first, because that always works and costs nothing: the earliest message that actually asks for
|
|
15
|
+
* something, cleaned up. Then, once there is enough to summarise, the model is asked for a short
|
|
16
|
+
* one — which is the only way to get "Stokvel dashboard with recharts" out of a conversation that
|
|
17
|
+
* happens to open with "hi". That ask is cheap, happens once, and never blocks a turn; when it
|
|
18
|
+
* fails the mechanical name stands.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Openers that say nothing about the work.
|
|
22
|
+
*
|
|
23
|
+
* Anchored, and matched against the whole message rather than its start: "hi" is a greeting and
|
|
24
|
+
* "highlight the failing test" is not, and a prefix match cannot tell them apart.
|
|
25
|
+
*/
|
|
26
|
+
const PLEASANTRY = new RegExp('^(?:'
|
|
27
|
+
+ [
|
|
28
|
+
'hi', 'hey', 'hello', 'yo', 'sup', 'ok', 'okay', 'k',
|
|
29
|
+
'thanks', 'thank you', 'ta', 'cheers',
|
|
30
|
+
'cool', 'nice', 'great', 'lovely', 'perfect',
|
|
31
|
+
// Both halves of a greeting, because "good morning" is two words and "morning" is one, and a
|
|
32
|
+
// list of single words matched neither.
|
|
33
|
+
'(?:good )?(?:morning|afternoon|evening|day)',
|
|
34
|
+
'(?:hi|hey|hello) there',
|
|
35
|
+
'please', 'continue', 'carry on', 'go on', 'go ahead', 'proceed',
|
|
36
|
+
'yes', 'yep', 'yeah', 'no', 'nope', 'sure', 'right', 'done', 'next',
|
|
37
|
+
'and\\?', 'well\\?',
|
|
38
|
+
].join('|')
|
|
39
|
+
+ ')[\\s!.,?]*$', 'i');
|
|
40
|
+
/** KONECK's own scaffolding, which a person did not write and must never be read as a request. */
|
|
41
|
+
const MACHINERY = /^\s*\[(continue|persistent session goal|effort|the request you are working on)/i;
|
|
42
|
+
export function isPleasantry(text) {
|
|
43
|
+
return PLEASANTRY.test(text.trim());
|
|
44
|
+
}
|
|
45
|
+
/** The text of a message, whatever shape its content came in. */
|
|
46
|
+
export function textOf(message) {
|
|
47
|
+
if (!message)
|
|
48
|
+
return '';
|
|
49
|
+
const content = message.content;
|
|
50
|
+
if (typeof content === 'string')
|
|
51
|
+
return content;
|
|
52
|
+
if (!Array.isArray(content))
|
|
53
|
+
return '';
|
|
54
|
+
return content
|
|
55
|
+
.filter((part) => !!part && typeof part === 'object')
|
|
56
|
+
.map(part => (part.type === 'text' && typeof part.text === 'string' ? part.text : ''))
|
|
57
|
+
.join(' ')
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A name from the conversation itself, with no model involved.
|
|
62
|
+
*
|
|
63
|
+
* The earliest thing said that asks for something: greetings are skipped, KONECK's own prefixes are
|
|
64
|
+
* skipped, and a message too short to mean anything is skipped. Falls back to the first message
|
|
65
|
+
* there is — because a session called "hi" is still better than one called nothing — and to a plain
|
|
66
|
+
* label when even that is missing.
|
|
67
|
+
*/
|
|
68
|
+
export function mechanicalTitle(messages, limit = 72) {
|
|
69
|
+
const fromUser = messages.filter(m => m.role === 'user').map(textOf);
|
|
70
|
+
const substantive = fromUser.find(text => {
|
|
71
|
+
const trimmed = text.trim();
|
|
72
|
+
if (trimmed === '' || MACHINERY.test(trimmed))
|
|
73
|
+
return false;
|
|
74
|
+
if (isPleasantry(trimmed))
|
|
75
|
+
return false;
|
|
76
|
+
// Three words or twelve characters: below that there is nothing to name.
|
|
77
|
+
return trimmed.length >= 12 || trimmed.split(/\s+/).length >= 3;
|
|
78
|
+
});
|
|
79
|
+
const chosen = substantive ?? fromUser.find(t => t.trim() !== '') ?? '';
|
|
80
|
+
return tidy(chosen, limit) || 'New session';
|
|
81
|
+
}
|
|
82
|
+
/** One line, no markdown fences, no newlines, trimmed to length at a word boundary. */
|
|
83
|
+
export function tidy(text, limit = 72) {
|
|
84
|
+
const oneLine = String(text ?? '')
|
|
85
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
86
|
+
.replace(/[`*_#>]/g, '')
|
|
87
|
+
.replace(/\s+/g, ' ')
|
|
88
|
+
.trim();
|
|
89
|
+
if (oneLine.length <= limit)
|
|
90
|
+
return oneLine;
|
|
91
|
+
const cut = oneLine.slice(0, limit);
|
|
92
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
93
|
+
return (lastSpace > limit * 0.6 ? cut.slice(0, lastSpace) : cut).replace(/[\s,.;:–-]+$/, '') + '…';
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Whether a session has enough in it to be worth naming properly.
|
|
97
|
+
*
|
|
98
|
+
* Asking a model to name "hi" wastes a call and produces a title about greetings. Two exchanges, or
|
|
99
|
+
* one substantial request, is the point at which there is something to summarise.
|
|
100
|
+
*/
|
|
101
|
+
export function worthNaming(messages) {
|
|
102
|
+
const fromUser = messages.filter(m => m.role === 'user').map(textOf)
|
|
103
|
+
.filter(t => t.trim() !== '' && !MACHINERY.test(t));
|
|
104
|
+
if (fromUser.length === 0)
|
|
105
|
+
return false;
|
|
106
|
+
const substantial = fromUser.some(t => !isPleasantry(t) && t.trim().length >= 20);
|
|
107
|
+
return substantial || fromUser.length >= 2;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* What to ask the model for.
|
|
111
|
+
*
|
|
112
|
+
* The instruction is narrow on purpose: a model asked to "summarise" writes a sentence, and a model
|
|
113
|
+
* asked for a title writes "Session Summary: Implementation of...". Naming the shape wanted, giving
|
|
114
|
+
* an example of the register, and forbidding the two habits it will otherwise reach for is what gets
|
|
115
|
+
* a usable answer back in one attempt rather than three.
|
|
116
|
+
*/
|
|
117
|
+
export function titleQuestion(messages, take = 8) {
|
|
118
|
+
const relevant = messages
|
|
119
|
+
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
120
|
+
.slice(0, take)
|
|
121
|
+
.map(m => `${m.role === 'user' ? 'asked' : 'did'}: ${tidy(textOf(m), 240)}`)
|
|
122
|
+
.filter(line => line.length > 8)
|
|
123
|
+
.join('\n');
|
|
124
|
+
return 'Name this coding session in three to six words, as a person would label a tab.\n\n'
|
|
125
|
+
+ relevant
|
|
126
|
+
+ '\n\nAnswer with the name alone: no quotes, no full stop, no prefix like "Session" or '
|
|
127
|
+
+ '"Task". Name what the work is about, not that it is a session. If the conversation opens '
|
|
128
|
+
+ 'with a greeting, ignore the greeting.';
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The model's answer, made safe to use as a title, or null when it is not usable.
|
|
132
|
+
*
|
|
133
|
+
* A model asked for six words will sometimes send a paragraph, a quoted phrase, a bulleted list or
|
|
134
|
+
* an apology. Anything that does not look like a label is refused, and the mechanical name stands —
|
|
135
|
+
* a bad title is worse than a plain one, because a picker full of "Certainly! Here is a title" is
|
|
136
|
+
* unreadable in a way that "hi" at least is not.
|
|
137
|
+
*/
|
|
138
|
+
export function usableTitle(answer, limit = 72) {
|
|
139
|
+
const first = String(answer ?? '')
|
|
140
|
+
.split('\n')
|
|
141
|
+
.map(line => line.trim())
|
|
142
|
+
.find(line => line !== '' && !/^[-*•\d.]+\s*$/.test(line));
|
|
143
|
+
if (!first)
|
|
144
|
+
return null;
|
|
145
|
+
/*
|
|
146
|
+
* The sentence test runs before the trim, not after.
|
|
147
|
+
*
|
|
148
|
+
* Trimming first made a paragraph look like a title: "This session covers building a ledger page
|
|
149
|
+
* with demo data and styling it..." is nineteen words, but cut to seventy-two characters it is
|
|
150
|
+
* eleven, and passed a check meant to reject exactly that.
|
|
151
|
+
*/
|
|
152
|
+
const withoutPrefix = first
|
|
153
|
+
.replace(/^["'`]+|["'`]+$/g, '')
|
|
154
|
+
.replace(/^(session|task|title|chat)\s*[:\-–]\s*/i, '')
|
|
155
|
+
.trim();
|
|
156
|
+
if (withoutPrefix.split(/\s+/).length > 12)
|
|
157
|
+
return null;
|
|
158
|
+
const cleaned = tidy(withoutPrefix, limit).replace(/[.!]+$/, '').trim();
|
|
159
|
+
if (cleaned.length < 3)
|
|
160
|
+
return null;
|
|
161
|
+
// A refusal or a preamble is not a title, however politely it is phrased.
|
|
162
|
+
if (/^(certainly|sure|here is|here's|of course|i cannot|i can't|as an ai)\b/i.test(cleaned)) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return cleaned;
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=session-title.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-title.js","sourceRoot":"","sources":["../src/session-title.ts"],"names":[],"mappings":"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"}
|
package/dist/web/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/web/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB/C,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAGrC,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA6BD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,wBAAsB,QAAQ,CAC5B,YAAY,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAClD,OAAO,CAAC,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/web/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB/C,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAGrC,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA6BD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,wBAAsB,QAAQ,CAC5B,YAAY,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAClD,OAAO,CAAC,SAAS,CAAC,CA44CpB"}
|
package/dist/web/api.js
CHANGED
|
@@ -131,7 +131,7 @@ export async function startWeb(baseConfigIn, options = {}) {
|
|
|
131
131
|
if (route === '/api/workspaces' && req.method === 'GET') {
|
|
132
132
|
if (!mayRead)
|
|
133
133
|
return json(res, 401, { error: 'token required' });
|
|
134
|
-
const trusted = await trustedWorkspaces().catch(() => []);
|
|
134
|
+
const trusted = await trustedWorkspaces(baseConfig.stateHome).catch(() => []);
|
|
135
135
|
// The launch directory is always offered even if it is not in the store, because getting
|
|
136
136
|
// this far means it was trusted or explicitly run with --ci.
|
|
137
137
|
const choices = [...new Set([baseConfig.cwd, ...trusted])];
|
|
@@ -213,6 +213,76 @@ export async function startWeb(baseConfigIn, options = {}) {
|
|
|
213
213
|
provider: info.provider,
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
|
+
/*
|
|
217
|
+
* The computer's own folder dialog, opened by the server.
|
|
218
|
+
*
|
|
219
|
+
* A browser cannot do this: webkitdirectory and showDirectoryPicker both withhold the
|
|
220
|
+
* absolute path deliberately. The server can, because it is on the same machine as the files
|
|
221
|
+
* and reaching this route already requires loopback and the acting token.
|
|
222
|
+
*
|
|
223
|
+
* Loopback is load-bearing here rather than incidental: the dialog appears on the machine
|
|
224
|
+
* running KONECK, so a server bound to a network address would be putting a window on
|
|
225
|
+
* somebody else's screen and waiting for them to answer it.
|
|
226
|
+
*/
|
|
227
|
+
if (route === '/api/pickfolder' && req.method === 'POST') {
|
|
228
|
+
if (!mayAct)
|
|
229
|
+
return json(res, 401, { error: 'token required' });
|
|
230
|
+
if (!isLocal) {
|
|
231
|
+
return json(res, 400, { error: 'This server is listening on a network address, so the dialog would open on the '
|
|
232
|
+
+ 'machine running KONECK rather than yours. Use the folder list instead.' });
|
|
233
|
+
}
|
|
234
|
+
const body = await readBody(req);
|
|
235
|
+
const { chooseFolder, dialogAvailable, hasDesktop } = await import('../folder-dialog.js');
|
|
236
|
+
if (!hasDesktop()) {
|
|
237
|
+
return json(res, 200, { available: false, reason: 'This machine has no desktop session, so there is no dialog to show — KONECK is '
|
|
238
|
+
+ 'probably running over SSH. Use the folder list instead.' });
|
|
239
|
+
}
|
|
240
|
+
if (!(await dialogAvailable())) {
|
|
241
|
+
return json(res, 200, { available: false, reason: 'No folder dialog is installed. On most desktops that is the zenity package, or '
|
|
242
|
+
+ 'kdialog on KDE. Use the folder list instead.' });
|
|
243
|
+
}
|
|
244
|
+
const chosen = await chooseFolder(String(body.startIn ?? '') || workspace, body.newFolder === true ? 'Choose where the new folder goes' : 'Open a project');
|
|
245
|
+
if (!chosen.ok) {
|
|
246
|
+
// Dismissing a dialog is a decision, not a failure, and is reported as one.
|
|
247
|
+
return json(res, 200, chosen.cancelled
|
|
248
|
+
? { available: true, cancelled: true }
|
|
249
|
+
: { available: false, reason: chosen.reason });
|
|
250
|
+
}
|
|
251
|
+
return json(res, 200, { available: true, path: chosen.path });
|
|
252
|
+
}
|
|
253
|
+
/*
|
|
254
|
+
* A new folder, made where the dialog said.
|
|
255
|
+
*
|
|
256
|
+
* Its own step rather than part of the dialog, because a folder dialog chooses an existing
|
|
257
|
+
* folder — asking one to invent a name is asking it to be a save dialog, and the two behave
|
|
258
|
+
* differently on every platform.
|
|
259
|
+
*/
|
|
260
|
+
if (route === '/api/newfolder' && req.method === 'POST') {
|
|
261
|
+
if (!mayAct)
|
|
262
|
+
return json(res, 401, { error: 'token required' });
|
|
263
|
+
if (!isLocal)
|
|
264
|
+
return json(res, 400, { error: 'not from a network address' });
|
|
265
|
+
const body = await readBody(req);
|
|
266
|
+
const inside = path.resolve(String(body.inside ?? '') || workspace);
|
|
267
|
+
const name = String(body.name ?? '').trim();
|
|
268
|
+
// A name, not a path: anything with a separator in it is somewhere else entirely.
|
|
269
|
+
if (!name || /[\\/]/.test(name) || name === '.' || name === '..') {
|
|
270
|
+
return json(res, 400, { error: 'give the folder a name, without slashes in it' });
|
|
271
|
+
}
|
|
272
|
+
const made = path.join(inside, name);
|
|
273
|
+
const { mkdir } = await import('fs/promises');
|
|
274
|
+
try {
|
|
275
|
+
await mkdir(made, { recursive: false });
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
const why = err.code === 'EEXIST'
|
|
279
|
+
? `${made} already exists`
|
|
280
|
+
: `could not create ${made}`;
|
|
281
|
+
return json(res, 400, { error: why });
|
|
282
|
+
}
|
|
283
|
+
announce(` Folder created from the browser: ${made}`);
|
|
284
|
+
return json(res, 200, { path: made });
|
|
285
|
+
}
|
|
216
286
|
if (route === '/api/browse' && req.method === 'GET') {
|
|
217
287
|
if (!mayRead)
|
|
218
288
|
return json(res, 401, { error: 'token required' });
|
|
@@ -282,9 +352,9 @@ export async function startWeb(baseConfigIn, options = {}) {
|
|
|
282
352
|
+ 'through it. Switch workspace at a terminal.' });
|
|
283
353
|
}
|
|
284
354
|
const wasKnown = wanted === path.resolve(baseConfig.cwd)
|
|
285
|
-
|| await isWorkspaceTrusted(wanted).catch(() => false);
|
|
355
|
+
|| await isWorkspaceTrusted(wanted, baseConfig.stateHome).catch(() => false);
|
|
286
356
|
if (!wasKnown) {
|
|
287
|
-
await trustWorkspace(wanted).catch(() => undefined);
|
|
357
|
+
await trustWorkspace(wanted, baseConfig.stateHome).catch(() => undefined);
|
|
288
358
|
announce(` Workspace opened from the browser, and trusted: ${wanted}`);
|
|
289
359
|
}
|
|
290
360
|
workspace = wanted;
|
|
@@ -342,7 +412,7 @@ export async function startWeb(baseConfigIn, options = {}) {
|
|
|
342
412
|
if (url.searchParams.get('scope') === 'workspace') {
|
|
343
413
|
return json(res, 200, await searchSessions(workspace, q));
|
|
344
414
|
}
|
|
345
|
-
const trusted = await trustedWorkspaces().catch(() => []);
|
|
415
|
+
const trusted = await trustedWorkspaces(baseConfig.stateHome).catch(() => []);
|
|
346
416
|
return json(res, 200, await searchAllWorkspaces([workspace, baseConfig.cwd, ...trusted], q));
|
|
347
417
|
}
|
|
348
418
|
if (route === '/api/sessions' && req.method === 'GET') {
|