aegis-desktop 0.8.24 → 0.8.27
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/lib/local/engine.js +78 -3
- package/lib/local/prompt.js +86 -2
- package/lib/local/tools.js +13 -1
- package/package.json +2 -1
- package/vendor/platform.js +130 -0
- package/vendor/session-store.js +32 -1
package/lib/local/engine.js
CHANGED
|
@@ -95,6 +95,30 @@ try {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The module that decides where this install's data directory IS
|
|
100
|
+
* (`$AEGISCODE_HOME`, else `~/.aegiscode`) — the same two-layout resolution as
|
|
101
|
+
* `platformLib` above, and for the same reason: the environment block must
|
|
102
|
+
* state the path this machine actually writes, not a path this file guessed.
|
|
103
|
+
*
|
|
104
|
+
* Reported live (DeepSeek, Windows, 2026-10-04): asked to check the AEGIS data
|
|
105
|
+
* directory, the model produced `C:\Users\Dator.aegiscode` — the home directory
|
|
106
|
+
* with the separator EATEN — and then ran
|
|
107
|
+
* `Get-ChildItem -Filter aegis -Recurse -Depth 2` over the whole profile when
|
|
108
|
+
* that path came back empty. Nothing in the prompt had ever named the real
|
|
109
|
+
* directory, so building it from the home directory was the only move it had.
|
|
110
|
+
*/
|
|
111
|
+
let credentialsLib = null;
|
|
112
|
+
try {
|
|
113
|
+
credentialsLib = require('../../../client/credentials.js');
|
|
114
|
+
} catch {
|
|
115
|
+
try {
|
|
116
|
+
credentialsLib = require('../../vendor/credentials.js');
|
|
117
|
+
} catch {
|
|
118
|
+
credentialsLib = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
/**
|
|
99
123
|
* The environment facts the model needs to stop asking which OS it is on.
|
|
100
124
|
*
|
|
@@ -167,11 +191,54 @@ function hostEnv(base = {}, supplied = {}, payload = null) {
|
|
|
167
191
|
} catch {
|
|
168
192
|
/* no desktop line */
|
|
169
193
|
}
|
|
194
|
+
// The data directory (`$AEGISCODE_HOME`, else `~/.aegiscode`), read from the
|
|
195
|
+
// module every host already resolves it through. Stated rather than left to
|
|
196
|
+
// be built from `homedir`: a model that concatenates home + ".aegiscode"
|
|
197
|
+
// itself is one dropped separator away from `Dator.aegiscode`, a path that
|
|
198
|
+
// does not exist, and it has no way to tell the difference from the answer.
|
|
199
|
+
let dataDir = b.dataDir;
|
|
200
|
+
try {
|
|
201
|
+
if (!dataDir && credentialsLib) dataDir = credentialsLib.aegisHome();
|
|
202
|
+
} catch {
|
|
203
|
+
/* no data-directory line */
|
|
204
|
+
}
|
|
205
|
+
// The user's own locale, from the module that already resolves home and
|
|
206
|
+
// Desktop (client/platform.js appLocale). Stated for the same reason the
|
|
207
|
+
// Desktop path is: it is a fact about THIS machine that the model cannot
|
|
208
|
+
// derive from `platform: win32`, and until it is stated the model mirrors
|
|
209
|
+
// whatever language the message arrived in and defaults to the prompt's
|
|
210
|
+
// English otherwise — so a Swedish user had no way to stop being greeted in
|
|
211
|
+
// English, and a one-word request ("städa") had no language to be read in.
|
|
212
|
+
// Best-effort: a failed probe simply omits the line.
|
|
213
|
+
let locale = b.locale;
|
|
214
|
+
let localeName = b.localeName;
|
|
215
|
+
try {
|
|
216
|
+
if (!locale && platformLib && typeof platformLib.appLocale === 'function') {
|
|
217
|
+
locale = platformLib.appLocale();
|
|
218
|
+
}
|
|
219
|
+
if (locale && !localeName && platformLib && typeof platformLib.languageName === 'function') {
|
|
220
|
+
localeName = platformLib.languageName(locale);
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
/* no locale line */
|
|
224
|
+
}
|
|
170
225
|
return {
|
|
171
226
|
platform: pick('platform', b.platform || process.platform),
|
|
172
227
|
arch: pick('arch', b.arch || process.arch),
|
|
173
228
|
homedir: pick('homedir', homedir),
|
|
174
229
|
desktopdir: pick('desktopdir', desktopdir),
|
|
230
|
+
dataDir: pick('dataDir', dataDir),
|
|
231
|
+
locale: pick('locale', locale),
|
|
232
|
+
localeName: pick('localeName', localeName),
|
|
233
|
+
// Durable language EVIDENCE, which is a different thing from `locale` and is
|
|
234
|
+
// passed through rather than probed: `locale` is where the machine is set
|
|
235
|
+
// up, this is what the holder's own saved writing is in. A host that has
|
|
236
|
+
// folded the avatar profile (lib/avatar/profile.js foldLanguage, which
|
|
237
|
+
// already emits "writes durably in Swedish (sv), so reply in Swedish unless
|
|
238
|
+
// asked otherwise") hands the facets in here; a host with no avatar passes
|
|
239
|
+
// nothing and gets the exact pre-locale turn. Shapes: a `{ code, name }`,
|
|
240
|
+
// a `{ key, value }` facet, or a bare string.
|
|
241
|
+
languages: pick('languages', b.languages),
|
|
175
242
|
cwd: pick('cwd', cwd),
|
|
176
243
|
shell: pick('shell', shellName),
|
|
177
244
|
shellKind: pick('shellKind', shellKind),
|
|
@@ -1486,12 +1553,20 @@ function createLocalEngine({
|
|
|
1486
1553
|
// and always disposed when the turn ends.
|
|
1487
1554
|
let shell = null;
|
|
1488
1555
|
const getShell = () => shell || (shell = new ShellSession({ cwd: envFor(payload).cwd }));
|
|
1489
|
-
const toolCtx = { getShell, signal };
|
|
1490
1556
|
// The turn's working directory. It rides on every tool frame because a
|
|
1491
1557
|
// tool row's args are the command or the path — never the directory the
|
|
1492
1558
|
// command runs in — which left "where is it working in" unanswerable from
|
|
1493
|
-
// the transcript.
|
|
1494
|
-
|
|
1559
|
+
// the transcript. It also has to ride on `toolCtx`, which is what
|
|
1560
|
+
// normalizeArgs (tools.js) actually resolves a RELATIVE readFile/writeFile/
|
|
1561
|
+
// editFile/listDir/glob/grep path against — omitting `cwd` here meant that
|
|
1562
|
+
// resolution fell back to the HOST PROCESS's own cwd (the Electron install
|
|
1563
|
+
// dir off Linux, per baseDir()'s comment in tools.js, which fixed this for
|
|
1564
|
+
// the no-path default but never for a path the model actually supplied).
|
|
1565
|
+
// `desktopdir` rides the same way so resolvePath can correct a model's
|
|
1566
|
+
// literal "Desktop" guess to the real one (platform.js's own alias fix).
|
|
1567
|
+
const turnEnv = envFor(payload);
|
|
1568
|
+
const turnCwd = turnEnv.cwd;
|
|
1569
|
+
const toolCtx = { getShell, signal, cwd: turnCwd, desktopdir: turnEnv.desktopdir };
|
|
1495
1570
|
|
|
1496
1571
|
try {
|
|
1497
1572
|
// byok's settings row lives under the provider named IN THE MODEL id
|
package/lib/local/prompt.js
CHANGED
|
@@ -70,16 +70,86 @@ const TOOL_LINE =
|
|
|
70
70
|
'across calls within the turn, like a real terminal. task spawns a specialist subagent ' +
|
|
71
71
|
'(its own tool loop, same model) for a focused, self-contained piece of work.';
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* One durable-language entry as a sentence fragment, accepting the three shapes
|
|
75
|
+
* a host can hand in without this file growing a dependency on the avatar
|
|
76
|
+
* module that produces them: a bare string, `{ code, name }`, or a folded
|
|
77
|
+
* profile facet `{ key, value }` (whose `value` is already prose —
|
|
78
|
+
* "writes durably in Swedish (sv), so reply in Swedish unless asked otherwise" —
|
|
79
|
+
* and is preferred as-is rather than re-rendered into something longer).
|
|
80
|
+
*/
|
|
81
|
+
function languageLine(entry) {
|
|
82
|
+
if (typeof entry === 'string') return entry.trim();
|
|
83
|
+
if (!entry || typeof entry !== 'object') return '';
|
|
84
|
+
if (typeof entry.value === 'string' && entry.value.trim()) return entry.value.trim();
|
|
85
|
+
const name = typeof entry.name === 'string' ? entry.name.trim() : '';
|
|
86
|
+
const code = typeof entry.code === 'string' ? entry.code.trim() : typeof entry.key === 'string' ? entry.key.trim() : '';
|
|
87
|
+
if (name && code) return `${name} (${code})`;
|
|
88
|
+
return name || code;
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
/**
|
|
74
92
|
* Render the environment preamble. Everything is optional: a missing field is
|
|
75
93
|
* simply omitted, so a headless caller can build a persona with nothing but
|
|
76
94
|
* the identity block.
|
|
77
95
|
*/
|
|
78
96
|
function environmentPreamble(env = {}) {
|
|
79
|
-
const {
|
|
80
|
-
|
|
97
|
+
const {
|
|
98
|
+
platform,
|
|
99
|
+
arch,
|
|
100
|
+
homedir,
|
|
101
|
+
desktopdir,
|
|
102
|
+
dataDir,
|
|
103
|
+
locale,
|
|
104
|
+
localeName,
|
|
105
|
+
languages,
|
|
106
|
+
cwd,
|
|
107
|
+
shell,
|
|
108
|
+
shellKind,
|
|
109
|
+
roots,
|
|
110
|
+
appVersion,
|
|
111
|
+
model,
|
|
112
|
+
} = env || {};
|
|
81
113
|
const bits = [];
|
|
82
114
|
if (platform) bits.push(`platform: ${platform}${arch ? ` (${arch})` : ''}`);
|
|
115
|
+
// The other half of "who and where is this machine": the platform line says
|
|
116
|
+
// what it runs, this says what language its user is in. It was the ONE
|
|
117
|
+
// environment fact a turn never carried, and the consequence was visible —
|
|
118
|
+
// the model mirrored whatever language the message arrived in and otherwise
|
|
119
|
+
// fell back to this prompt's English, so a Swedish user could not stop being
|
|
120
|
+
// greeted in English and a one-word request ("städa") arrived with no
|
|
121
|
+
// language to be read in.
|
|
122
|
+
if (locale) {
|
|
123
|
+
const base = String(locale).split('-')[0].toLowerCase();
|
|
124
|
+
const name = localeName ? ` (${localeName})` : '';
|
|
125
|
+
// The instruction is only load-bearing for a NON-English host: English is
|
|
126
|
+
// this prompt's own language, so "reply in English" would be noise that
|
|
127
|
+
// competes with whatever language the request is actually written in.
|
|
128
|
+
bits.push(
|
|
129
|
+
base === 'en'
|
|
130
|
+
? `locale: ${locale}${name}`
|
|
131
|
+
: `locale: ${locale}${name} — the user's own machine and account run in this language, ` +
|
|
132
|
+
'so expect requests written in it and answer in the language the user writes to you ' +
|
|
133
|
+
'in. Use this to resolve an otherwise ambiguous request (a bare path, a pasted ' +
|
|
134
|
+
'error, a one-word command) — never to override a request written in another language.'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
// Durable EVIDENCE, which is deliberately a separate line from `locale`: one
|
|
138
|
+
// is how the machine is configured, the other is what the user's own saved
|
|
139
|
+
// writing is in, and they can disagree (an English-configured machine used by
|
|
140
|
+
// someone who writes Swedish all day). The request in front of the model
|
|
141
|
+
// still outranks both, so the line says so rather than reading as an order to
|
|
142
|
+
// switch languages.
|
|
143
|
+
const langLines = (Array.isArray(languages) ? languages : [])
|
|
144
|
+
.map((entry) => languageLine(entry))
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
if (langLines.length) {
|
|
147
|
+
bits.push(
|
|
148
|
+
`recorded language evidence from the user's own saved writing: ${langLines.join('; ')} — ` +
|
|
149
|
+
'the language of the request in front of you always wins; this is evidence, not an ' +
|
|
150
|
+
'instruction to switch'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
83
153
|
if (homedir) bits.push(`home directory: ${homedir}`);
|
|
84
154
|
// Named explicitly because `~/Desktop` is a GUESS that is wrong on two of the
|
|
85
155
|
// three platforms this app ships to: OneDrive folder redirection moves it on
|
|
@@ -93,6 +163,20 @@ function environmentPreamble(env = {}) {
|
|
|
93
163
|
'called "Desktop" there',
|
|
94
164
|
);
|
|
95
165
|
}
|
|
166
|
+
// Same failure mode as the desktop line, one directory over, and reported
|
|
167
|
+
// live: asked about the AEGIS data directory, DeepSeek emitted
|
|
168
|
+
// `C:\Users\Dator.aegiscode` — the home directory with the separator dropped —
|
|
169
|
+
// got an empty listing, and concluded the store did not exist. The path is
|
|
170
|
+
// given verbatim AND the wrong form is named, because a model reading a
|
|
171
|
+
// missing path cannot tell a typo from an empty directory.
|
|
172
|
+
if (dataDir) {
|
|
173
|
+
bits.push(
|
|
174
|
+
`AEGIS data directory: ${dataDir} — this is the store itself (config, sessions, ` +
|
|
175
|
+
'credentials, queue). Use this exact path; do NOT append it to the home directory ' +
|
|
176
|
+
'yourself, because a dropped separator ("C:\\Users\\you.aegiscode") names a path that ' +
|
|
177
|
+
'does not exist and looks like an empty directory rather than a mistake',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
96
180
|
if (cwd) bits.push(`working directory: ${cwd}`);
|
|
97
181
|
if (shell) bits.push(`exec shell: ${shell}${shellKind ? ` (${shellKind})` : ''}`);
|
|
98
182
|
if (model) bits.push(`model: ${model}`);
|
package/lib/local/tools.js
CHANGED
|
@@ -375,7 +375,19 @@ function normalizeArgs(name, args, ctx = {}) {
|
|
|
375
375
|
for (const key of keys) {
|
|
376
376
|
const value = input[key];
|
|
377
377
|
if (typeof value !== 'string' || !value.trim()) continue; // absent → the executor's default
|
|
378
|
-
const res = platform.resolvePath(value, {
|
|
378
|
+
const res = platform.resolvePath(value, {
|
|
379
|
+
// The tool layer's platform seam, the same one `client/platform.js`
|
|
380
|
+
// takes on every function and `shell.js` already passes at its call
|
|
381
|
+
// site: `ctx.platform` names the host whose path rules apply. Absent
|
|
382
|
+
// (every existing caller), `resolvePath` falls back to
|
|
383
|
+
// `process.platform`, so this changes nothing at runtime — but it is
|
|
384
|
+
// what lets the win32 and darwin branches of THIS layer be exercised
|
|
385
|
+
// from a Linux CI, instead of being reachable only on a foreign host.
|
|
386
|
+
// See test/platform-parity.test.mjs.
|
|
387
|
+
platform: ctx && ctx.platform ? ctx.platform : undefined,
|
|
388
|
+
cwd: ctx && ctx.cwd ? ctx.cwd : undefined,
|
|
389
|
+
desktopdir: ctx && ctx.desktopdir ? ctx.desktopdir : undefined,
|
|
390
|
+
});
|
|
379
391
|
if (!res.ok) return { ok: false, args: input, error: `${name}: ${key} — ${res.error}` };
|
|
380
392
|
if (res.path !== value) {
|
|
381
393
|
if (out === input) out = { ...input };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.27",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"test:budget": "node ../test/budget.test.mjs",
|
|
39
39
|
"test:foreign-memory": "node ../test/foreign-memory.test.mjs",
|
|
40
40
|
"test:tools": "node ../test/local-tools.test.mjs",
|
|
41
|
+
"test:platform-parity": "node ../test/platform-parity.test.mjs",
|
|
41
42
|
"test:engine": "node ../test/local-engine.test.mjs",
|
|
42
43
|
"test:highlight": "node ../test/aegis-highlight.test.mjs",
|
|
43
44
|
"test:markdown": "node ../test/markdown.test.mjs",
|
package/vendor/platform.js
CHANGED
|
@@ -438,6 +438,97 @@ function hasDisplay(options = {}) {
|
|
|
438
438
|
// homeDir / resolvePath
|
|
439
439
|
// ---------------------------------------------------------------------------
|
|
440
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Normalise anything a machine calls a locale into a BCP-47 `ll` / `ll-CC`
|
|
443
|
+
* code, or `''` for "no answer".
|
|
444
|
+
*
|
|
445
|
+
* Four spellings reach this function and only the first is already usable:
|
|
446
|
+
* `sv-SE`, the POSIX `sv_SE.UTF-8` (and `sv_SE.utf8`, `sv_SE`), the GNU
|
|
447
|
+
* `LANGUAGE` list (`sv:en:de` — the caller splits it), and the two settings
|
|
448
|
+
* that mean NOT a language: `C` and `POSIX`, which say "no locale configured"
|
|
449
|
+
* and must never render as a language called "c". The modifier tail
|
|
450
|
+
* (`@euro`, `@latin`) is dropped: it changes collation, not language.
|
|
451
|
+
*/
|
|
452
|
+
function normalizeLocale(value) {
|
|
453
|
+
if (value == null) return '';
|
|
454
|
+
const raw = String(value).trim();
|
|
455
|
+
if (!raw) return '';
|
|
456
|
+
const head = raw.split('@')[0].split('.')[0];
|
|
457
|
+
if (/^(c|posix)$/i.test(head)) return '';
|
|
458
|
+
const m = /^([A-Za-z]{2,3})(?:[_-]([A-Za-z0-9]{2,8}))?$/.exec(head);
|
|
459
|
+
if (!m) return '';
|
|
460
|
+
const lang = m[1].toLowerCase();
|
|
461
|
+
if (lang === 'c' || lang === 'posix') return '';
|
|
462
|
+
return m[2] ? `${lang}-${m[2].toUpperCase()}` : lang;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* The language the user's own machine and account run in — `''` when nothing
|
|
467
|
+
* can say.
|
|
468
|
+
*
|
|
469
|
+
* The model has always been told the platform, the home directory and the real
|
|
470
|
+
* Desktop path, but never this, so a Swedish user's DeepSeek turn started with
|
|
471
|
+
* no field saying the machine is Swedish: the model mirrored whatever language
|
|
472
|
+
* arrived in the message and defaulted to the prompt's English otherwise.
|
|
473
|
+
* Mirrored language is not the same as known language — an up-front
|
|
474
|
+
* `locale: sv-SE` is what lets it answer a Swedish one-word request ("städa")
|
|
475
|
+
* in Swedish instead of asking which language to use.
|
|
476
|
+
*
|
|
477
|
+
* POSIX order is the order below (`LC_ALL` overrides `LC_CTYPE` overrides
|
|
478
|
+
* `LC_MESSAGES` overrides `LANG`); `LANGUAGE` is a GNU preference LIST and is
|
|
479
|
+
* consulted last only for its first entry. Windows sets none of these, and
|
|
480
|
+
* macOS sets none of them for an app launched from Finder, so `Intl` is the
|
|
481
|
+
* fallback — it reads the ICU default the runtime was started with, which is
|
|
482
|
+
* the same answer the OS gives the app. An `env` the caller INJECTED is trusted
|
|
483
|
+
* as given (never topped up from `Intl`), which is what makes a test with
|
|
484
|
+
* `env: {}` deterministic instead of dependent on the CI runner's locale.
|
|
485
|
+
*/
|
|
486
|
+
function appLocale(options = {}) {
|
|
487
|
+
const { platform, env, o } = seams(options);
|
|
488
|
+
if (typeof o.locale === 'string') return normalizeLocale(o.locale);
|
|
489
|
+
const injected = o.env && typeof o.env === 'object';
|
|
490
|
+
for (const key of ['LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LANG']) {
|
|
491
|
+
const hit = normalizeLocale(envValue(env, key, platform));
|
|
492
|
+
if (hit) return hit;
|
|
493
|
+
}
|
|
494
|
+
const list = String(envValue(env, 'LANGUAGE', platform) || '');
|
|
495
|
+
if (list) {
|
|
496
|
+
// A colon list is a PREFERENCE order, and an empty field in it (":de" — how
|
|
497
|
+
// a launch script that cleared the first entry leaves it) means "skip this
|
|
498
|
+
// one", not "the answer is nothing".
|
|
499
|
+
for (const part of list.split(':')) {
|
|
500
|
+
const first = normalizeLocale(part);
|
|
501
|
+
if (first) return first;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (injected) return ''; // the caller owns the environment; it said nothing
|
|
505
|
+
try {
|
|
506
|
+
return normalizeLocale(new Intl.DateTimeFormat().resolvedOptions().locale);
|
|
507
|
+
} catch {
|
|
508
|
+
return '';
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The English name of a language code — `Swedish` for `sv`, `''` when the
|
|
514
|
+
* runtime has no display table for it (Node without full ICU). Named in
|
|
515
|
+
* ENGLISH on purpose: it is interpolated into the English system prompt, so a
|
|
516
|
+
* Swedish host must not render it as "svenska".
|
|
517
|
+
*/
|
|
518
|
+
function languageName(code, options = {}) {
|
|
519
|
+
const { o } = seams(options);
|
|
520
|
+
if (typeof o.languageName === 'string') return o.languageName;
|
|
521
|
+
const hit = normalizeLocale(code);
|
|
522
|
+
if (!hit) return '';
|
|
523
|
+
const base = hit.split('-')[0];
|
|
524
|
+
try {
|
|
525
|
+
const display = new Intl.DisplayNames(['en'], { type: 'language' });
|
|
526
|
+
return display.of(base) || '';
|
|
527
|
+
} catch {
|
|
528
|
+
return '';
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
441
532
|
/**
|
|
442
533
|
* This user's home directory, `''` when nothing can say what it is.
|
|
443
534
|
*
|
|
@@ -628,6 +719,36 @@ function desktopDir(options = {}) {
|
|
|
628
719
|
return '';
|
|
629
720
|
}
|
|
630
721
|
|
|
722
|
+
/**
|
|
723
|
+
* If `work` names this user's home "Desktop" folder by its generic literal
|
|
724
|
+
* name — the one a model guesses, in English, regardless of locale or
|
|
725
|
+
* OneDrive redirection — rewrite it to the REAL desktop directory the caller
|
|
726
|
+
* already asked Windows/XDG for (`desktopDir()` above).
|
|
727
|
+
*
|
|
728
|
+
* The environment preamble already NAMES the real directory and tells the
|
|
729
|
+
* model to copy it verbatim rather than build it — but that is advisory, and
|
|
730
|
+
* a model that still writes `~/Desktop/notes.txt` was silently creating (and
|
|
731
|
+
* reporting success into) a spurious English "Desktop" folder next to the
|
|
732
|
+
* user's real one, which on a redirected or localised Windows session is a
|
|
733
|
+
* different, empty directory the user never sees. This is the opposite of
|
|
734
|
+
* desktopDir()'s own job: that function stops US from guessing a locale name;
|
|
735
|
+
* this stops a model's literal "Desktop" guess from winning when we were
|
|
736
|
+
* already told the real answer.
|
|
737
|
+
*
|
|
738
|
+
* No-op when no `desktopdir` was supplied, when it is simply `home/Desktop`
|
|
739
|
+
* (nothing to correct), or when `work` is not rooted at the guessed path.
|
|
740
|
+
*/
|
|
741
|
+
function applyDesktopAlias(work, { home, desktopdir, win32, P }) {
|
|
742
|
+
if (!home || !desktopdir) return work;
|
|
743
|
+
const guessed = P.join(home, 'Desktop');
|
|
744
|
+
const fold = (s) => (win32 ? s.toLowerCase() : s);
|
|
745
|
+
if (fold(guessed) === fold(desktopdir)) return work;
|
|
746
|
+
if (fold(work) === fold(guessed)) return desktopdir;
|
|
747
|
+
const prefix = guessed + P.sep;
|
|
748
|
+
if (fold(work).startsWith(fold(prefix))) return desktopdir + P.sep + work.slice(prefix.length);
|
|
749
|
+
return work;
|
|
750
|
+
}
|
|
751
|
+
|
|
631
752
|
/** `C:\…` / `C:/…` — a drive-qualified Windows path. */
|
|
632
753
|
const WIN_DRIVE_RE = /^[A-Za-z]:[\\/]/;
|
|
633
754
|
/** `C:` with nothing after it — drive-relative, and never what a model means. */
|
|
@@ -703,6 +824,12 @@ function resolvePath(input, options = {}) {
|
|
|
703
824
|
);
|
|
704
825
|
}
|
|
705
826
|
|
|
827
|
+
// ── the desktop alias ─────────────────────────────────────────────────────
|
|
828
|
+
// Runs before the wrong-OS check below because a corrected `work` is always
|
|
829
|
+
// native by construction (desktopDir() returns a real path for THIS
|
|
830
|
+
// platform), exactly like a `~` expansion already is.
|
|
831
|
+
work = applyDesktopAlias(work, { home: homeDir(options), desktopdir: o.desktopdir, win32, P });
|
|
832
|
+
|
|
706
833
|
// ── absolute-for-the-wrong-OS ─────────────────────────────────────────────
|
|
707
834
|
// Skipped after a `~` expansion: that result is native by construction.
|
|
708
835
|
if (!expandedHome) {
|
|
@@ -755,6 +882,9 @@ const api = {
|
|
|
755
882
|
desktopDir,
|
|
756
883
|
resolvePath,
|
|
757
884
|
hasPosixModes,
|
|
885
|
+
appLocale,
|
|
886
|
+
languageName,
|
|
887
|
+
normalizeLocale,
|
|
758
888
|
};
|
|
759
889
|
|
|
760
890
|
module.exports = api;
|
package/vendor/session-store.js
CHANGED
|
@@ -288,6 +288,31 @@ function recordExchange(dir, exchange) {
|
|
|
288
288
|
});
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Stamp a conversation's name.
|
|
293
|
+
*
|
|
294
|
+
* The name is what the session LIST shows (the desktop app, the MCP plugin and
|
|
295
|
+
* `/resume` all read it from here or from their own mirror), and it is NOT the
|
|
296
|
+
* raw first message: `recordExchange` seeds `title` with the prompt so a
|
|
297
|
+
* session has something to show before anything has named it, and this replaces
|
|
298
|
+
* that seed with the real name once one exists (the CLI's `naming.js`).
|
|
299
|
+
*
|
|
300
|
+
* An empty name is refused rather than written: a namer that failed must not
|
|
301
|
+
* blank a name that is already there.
|
|
302
|
+
*/
|
|
303
|
+
function setTitle(dir, sessionId, title) {
|
|
304
|
+
const clean = String(title == null ? '' : title).trim().slice(0, 200);
|
|
305
|
+
if (!sessionId || !clean) return null;
|
|
306
|
+
return mutate(dir, (sessions) => {
|
|
307
|
+
const session = sessions[sessionId];
|
|
308
|
+
if (!session) return null;
|
|
309
|
+
session.title = clean;
|
|
310
|
+
session.updatedAt = Date.now();
|
|
311
|
+
session.seq = nextSeq(sessions);
|
|
312
|
+
return session;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
291
316
|
function listSessions(dir) {
|
|
292
317
|
const sessions = load(dir);
|
|
293
318
|
return Object.values(sessions)
|
|
@@ -389,10 +414,15 @@ function listSummaries(dir, limit = 8) {
|
|
|
389
414
|
.map((s) => {
|
|
390
415
|
const messages = Array.isArray(s.messages) ? s.messages : [];
|
|
391
416
|
const first = messages.find((m) => m && m.role === 'user');
|
|
417
|
+
// A named session lists by its name. `title` is seeded from the prompt at
|
|
418
|
+
// record time, so for an unnamed session this is the same string the
|
|
419
|
+
// first message would have produced — no visible change — while a real
|
|
420
|
+
// name (setTitle) wins over the raw prompt, which is the whole point of
|
|
421
|
+
// naming a conversation.
|
|
392
422
|
return {
|
|
393
423
|
id: s.id,
|
|
394
424
|
cwd: (s.cwd || '').split(/[\\/]/).filter(Boolean).pop() || '~',
|
|
395
|
-
summary: String((first && first.content) ||
|
|
425
|
+
summary: String(s.title || (first && first.content) || '').slice(0, 60),
|
|
396
426
|
time: new Date(s.updatedAt || Date.now()).toISOString(),
|
|
397
427
|
own: true,
|
|
398
428
|
origin: s.origin || 'unknown',
|
|
@@ -499,6 +529,7 @@ module.exports = {
|
|
|
499
529
|
upsertSession,
|
|
500
530
|
appendMessage,
|
|
501
531
|
recordExchange,
|
|
532
|
+
setTitle,
|
|
502
533
|
listSessions,
|
|
503
534
|
getSession,
|
|
504
535
|
deleteSession,
|