aegis-desktop 0.8.23 → 0.8.26
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 +4 -1
- package/package.json +1 -1
- package/vendor/platform.js +184 -8
- 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,10 @@ 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
|
+
cwd: ctx && ctx.cwd ? ctx.cwd : undefined,
|
|
380
|
+
desktopdir: ctx && ctx.desktopdir ? ctx.desktopdir : undefined,
|
|
381
|
+
});
|
|
379
382
|
if (!res.ok) return { ok: false, args: input, error: `${name}: ${key} — ${res.error}` };
|
|
380
383
|
if (res.path !== value) {
|
|
381
384
|
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.26",
|
|
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",
|
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
|
*
|
|
@@ -493,17 +584,56 @@ function hasPosixModes(options = {}) {
|
|
|
493
584
|
return !win32;
|
|
494
585
|
}
|
|
495
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Windows' own answer for "where is this user's Desktop", straight from the
|
|
589
|
+
* registry key Explorer itself reads and OneDrive itself rewrites when it
|
|
590
|
+
* redirects a folder. This is the one source immune to BOTH ways `~/Desktop`
|
|
591
|
+
* guesses wrong at once: it already names the OneDrive path when redirection
|
|
592
|
+
* is active, and it already names the LOCALISED folder — Swedish "Skrivbord",
|
|
593
|
+
* French "Bureau", whatever the session's physical folder is actually called
|
|
594
|
+
* — because it is Windows' own record of a real path, not an English-language
|
|
595
|
+
* guess with a `?` for an ending. Guessing per-language folder names instead
|
|
596
|
+
* would mean enumerating every language Windows ships, forever one behind;
|
|
597
|
+
* asking Windows is both simpler and exact.
|
|
598
|
+
*
|
|
599
|
+
* "Shell Folders" (not "User Shell Folders") is the one queried, because it
|
|
600
|
+
* holds the already-expanded absolute path — "User Shell Folders" is allowed
|
|
601
|
+
* to hold `%USERPROFILE%`-relative values, and this function has no reason to
|
|
602
|
+
* carry its own copy of Windows' expansion rules when the other key already
|
|
603
|
+
* did that work.
|
|
604
|
+
*
|
|
605
|
+
* Returns `''` on anything but a clean hit: `reg.exe` missing, a non-zero
|
|
606
|
+
* exit, unparsable output, or a path the filesystem does not actually have —
|
|
607
|
+
* each of those falls through to the candidate-guessing list in desktopDir()
|
|
608
|
+
* below, which is where this function's absence has always been tolerated.
|
|
609
|
+
*/
|
|
610
|
+
function winShellFolderDesktop(probe, dirExists) {
|
|
611
|
+
const out = probe('reg', [
|
|
612
|
+
'query',
|
|
613
|
+
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders',
|
|
614
|
+
'/v', 'Desktop',
|
|
615
|
+
]);
|
|
616
|
+
if (!out) return '';
|
|
617
|
+
const m = /^\s*Desktop\s+REG_(?:EXPAND_)?SZ\s+(.+)$/im.exec(out);
|
|
618
|
+
const value = m && m[1].trim();
|
|
619
|
+
return value && dirExists(value) ? value : '';
|
|
620
|
+
}
|
|
621
|
+
|
|
496
622
|
/**
|
|
497
623
|
* This user's real Desktop directory, or `''` when none can be found.
|
|
498
624
|
*
|
|
499
625
|
* `~/Desktop` is a GUESS, and it is wrong on both of the hosts this function was
|
|
500
626
|
* added for:
|
|
501
627
|
*
|
|
502
|
-
* - Windows with OneDrive. Folder redirection moves
|
|
503
|
-
* `%USERPROFILE%\OneDrive\Desktop`
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
628
|
+
* - Windows with OneDrive, or a localised session. Folder redirection moves
|
|
629
|
+
* the live Desktop to `%USERPROFILE%\OneDrive\Desktop` (or renames it
|
|
630
|
+
* entirely, e.g. Swedish "Skrivbord"), and commonly leaves an empty
|
|
631
|
+
* `%USERPROFILE%\Desktop` behind, so a write to the obvious English path
|
|
632
|
+
* lands in a directory the user is not looking at — or at a name that
|
|
633
|
+
* does not exist on their machine at all. `winShellFolderDesktop()` above
|
|
634
|
+
* asks Windows directly and settles both cases at once; the `%OneDrive%`-
|
|
635
|
+
* and `home`-based guesses below only run when that lookup comes back
|
|
636
|
+
* empty (no `reg.exe`, or the path it names no longer exists).
|
|
507
637
|
* - A localised Linux desktop. XDG translates the directory ITSELF, not just
|
|
508
638
|
* its label: a Swedish session has `~/Skrivbord` and no `~/Desktop` at all.
|
|
509
639
|
* `$XDG_DESKTOP_DIR` answers it, and `~/.config/user-dirs.dirs` is where
|
|
@@ -513,15 +643,22 @@ function hasPosixModes(options = {}) {
|
|
|
513
643
|
*
|
|
514
644
|
* Seams: `dirExists` (default: a real statSync isDirectory probe), `dirNonEmpty`
|
|
515
645
|
* (default: a real readdirSync length check, used only to break the win32
|
|
516
|
-
* OneDrive-vs-plain
|
|
517
|
-
* `
|
|
646
|
+
* OneDrive-vs-plain guess below when the registry lookup did not settle it),
|
|
647
|
+
* `probe` (default: a real `reg.exe` spawn, `null` on any failure) and
|
|
648
|
+
* `readText` (default: a real utf8 read, `null` on any failure), so every
|
|
649
|
+
* branch is exercised on one host.
|
|
518
650
|
*/
|
|
519
651
|
function desktopDir(options = {}) {
|
|
520
|
-
const { platform, env, win32, o } = seams(options);
|
|
652
|
+
const { platform, env, win32, probe, o } = seams(options);
|
|
521
653
|
const P = win32 ? path.win32 : path.posix;
|
|
522
654
|
const home = homeDir(options);
|
|
523
655
|
const dirExists = typeof o.dirExists === 'function' ? o.dirExists : defaultDirExists;
|
|
524
656
|
const dirNonEmpty = typeof o.dirNonEmpty === 'function' ? o.dirNonEmpty : defaultDirNonEmpty;
|
|
657
|
+
|
|
658
|
+
if (win32) {
|
|
659
|
+
const fromRegistry = winShellFolderDesktop(probe, dirExists);
|
|
660
|
+
if (fromRegistry) return fromRegistry;
|
|
661
|
+
}
|
|
525
662
|
const readText = typeof o.readText === 'function' ? o.readText : defaultReadText;
|
|
526
663
|
|
|
527
664
|
const candidates = [];
|
|
@@ -582,6 +719,36 @@ function desktopDir(options = {}) {
|
|
|
582
719
|
return '';
|
|
583
720
|
}
|
|
584
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
|
+
|
|
585
752
|
/** `C:\…` / `C:/…` — a drive-qualified Windows path. */
|
|
586
753
|
const WIN_DRIVE_RE = /^[A-Za-z]:[\\/]/;
|
|
587
754
|
/** `C:` with nothing after it — drive-relative, and never what a model means. */
|
|
@@ -657,6 +824,12 @@ function resolvePath(input, options = {}) {
|
|
|
657
824
|
);
|
|
658
825
|
}
|
|
659
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
|
+
|
|
660
833
|
// ── absolute-for-the-wrong-OS ─────────────────────────────────────────────
|
|
661
834
|
// Skipped after a `~` expansion: that result is native by construction.
|
|
662
835
|
if (!expandedHome) {
|
|
@@ -709,6 +882,9 @@ const api = {
|
|
|
709
882
|
desktopDir,
|
|
710
883
|
resolvePath,
|
|
711
884
|
hasPosixModes,
|
|
885
|
+
appLocale,
|
|
886
|
+
languageName,
|
|
887
|
+
normalizeLocale,
|
|
712
888
|
};
|
|
713
889
|
|
|
714
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,
|