talos-code 0.1.0 → 0.2.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +9 -4
  3. package/dist/args.js +4 -1
  4. package/dist/commands/provider-cli.js +146 -87
  5. package/dist/config/types.js +1 -1
  6. package/dist/i18n/en/approval.js +3 -0
  7. package/dist/i18n/en/credentials.js +10 -0
  8. package/dist/i18n/en/errors.js +21 -0
  9. package/dist/i18n/en/firstrun.js +12 -0
  10. package/dist/i18n/en/screen.js +38 -0
  11. package/dist/i18n/error-view.js +1 -1
  12. package/dist/main.js +2 -1
  13. package/dist/provider/openrouter-login.js +172 -0
  14. package/dist/runtime/context-archive.js +69 -0
  15. package/dist/runtime/repo.js +11 -0
  16. package/dist/runtime/supervisor.js +5 -2
  17. package/dist/runtime/talos-composition.js +9 -3
  18. package/dist/tui/agent-view.js +51 -0
  19. package/dist/tui/app.js +364 -27
  20. package/dist/tui/catalog-service.js +2 -1
  21. package/dist/tui/components/status-indicator.js +10 -4
  22. package/dist/tui/components/terminal-shell.js +10 -1
  23. package/dist/tui/descendant-approvals.js +104 -0
  24. package/dist/tui/launch-state.js +9 -0
  25. package/dist/tui/live-activity.js +13 -2
  26. package/dist/tui/overlays/agent-tree.js +7 -3
  27. package/dist/tui/overlays/approval-dialog.js +3 -1
  28. package/dist/tui/overlays/model-picker.js +5 -0
  29. package/dist/tui/overlays/provider-picker.js +26 -0
  30. package/dist/tui/project-trust-prompt.js +5 -3
  31. package/dist/tui/session-controller.js +25 -12
  32. package/dist/tui/setup-wizard.js +46 -0
  33. package/dist/tui/slash-commands.js +3 -1
  34. package/dist/tui/theme-catalog.js +3 -1
  35. package/dist/version.js +1 -1
  36. package/dist/workspace/checkpoint-store.js +32 -1
  37. package/dist/workspace/checkpoint.js +40 -12
  38. package/package.json +3 -1
  39. package/vendor/harness-ui/src/kernel/talosHarness.mjs +149 -2
  40. package/vendor/manifest.json +2 -2
@@ -0,0 +1,172 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ /*
10
+ * P21 (owner, 2026-09-26: "la possibilità di fare accesso oauth con Open Router, come fa l'applicazione desktop"; decisions
11
+ * the same day): sign in to OpenRouter from the CLI without pasting a key. The end of the flow is a plain OpenRouter API key,
12
+ * stored like a pasted one.
13
+ *
14
+ * The PKCE arithmetic is the desktop's own (`harness-ui/src/openrouter-oauth.mjs`, researched 2026-09-10 on OpenRouter's
15
+ * OAuth PKCE docs and RFC 7636): S256 always, the verifier never leaves this process, a single-use code that OpenRouter lets
16
+ * live 10 minutes, `state` carried in the callback PATH because OpenRouter documents no `state`, and error codes without
17
+ * response bodies. This file adds what a CLI needs, read on 2026-09-26:
18
+ * - a one-shot loopback server on 127.0.0.1 and a port the system picks, open only while the sign-in lasts (RFC 8252 §7.3,
19
+ * §8.3: the IP literal, not `localhost`; listen on loopback only; close the port once the answer is in); the system browser,
20
+ * never an embedded one (§8.12). Pi (`packages/ai/src/auth/oauth/openrouter.ts`) and Hermes (`hermes_cli/auth_openrouter.py`)
21
+ * do the same, Codex for its own sign-in (`codex-rs/login/src/server.rs`);
22
+ * - a code pasted by hand, raced against the browser (Pi): the code alone or the whole address the browser landed on;
23
+ * - a remote session (SSH) goes to OpenRouter's no-callback mode at once, where the page SHOWS the code (Hermes).
24
+ */
25
+ import { spawn } from 'node:child_process';
26
+ import { createServer } from 'node:http';
27
+ import { join } from 'node:path';
28
+ import { pathToFileURL } from 'node:url';
29
+ /** The desktop's PKCE module, from the checkout or from the package's own copy of the kernel (`vendor/harness-ui`). */
30
+ export async function loadOpenRouterOAuth(repoRoot) {
31
+ return await import(__rewriteRelativeImportExtension(pathToFileURL(join(repoRoot, 'harness-ui', 'src', 'openrouter-oauth.mjs')).href));
32
+ }
33
+ /** What OpenRouter writes beside the key on the person's key page, so they can tell which program asked and revoke it. */
34
+ export const LOGIN_LABEL = 'TALOS CLI';
35
+ /** OpenRouter's code lives 10 minutes ("The code is single-use and expires after 10 minutes"); the wait is the same. */
36
+ export const LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
37
+ const EXCHANGE_TIMEOUT_MS = 30_000;
38
+ export function isRemoteSession(env = process.env) {
39
+ return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
40
+ }
41
+ /** The code in what the person pasted: the code itself, or the address (or query) the browser landed on. */
42
+ export function codeFromInput(input) {
43
+ const value = String(input ?? '').trim();
44
+ if (!value)
45
+ return null;
46
+ try {
47
+ const url = new URL(value);
48
+ return url.searchParams.get('code') || null;
49
+ }
50
+ catch { /* not an address */ }
51
+ if (value.includes('code=')) {
52
+ const code = new URLSearchParams(value.replace(/^[^?]*\?/u, '')).get('code');
53
+ return code || null;
54
+ }
55
+ return /\s/u.test(value) ? null : value;
56
+ }
57
+ /**
58
+ * Opens the address in the system browser, without a shell (on Windows `&` in an address would split a `cmd` line).
59
+ * A test profile never opens a window: the address is only shown.
60
+ */
61
+ export function openInBrowser(url, { env = process.env, platform = process.platform, spawnImpl = spawn } = {}) {
62
+ if (env.TALOS_TEST_PROFILE === '1')
63
+ return false;
64
+ if (!/^https:\/\//u.test(url))
65
+ return false;
66
+ const [command, args] = platform === 'win32' ? ['rundll32', ['url.dll,FileProtocolHandler', url]] : platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]];
67
+ try {
68
+ const child = spawnImpl(command, args, { detached: true, stdio: 'ignore', windowsHide: true });
69
+ child.on('error', () => { });
70
+ child.unref();
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ export class OpenRouterLoginError extends Error {
78
+ code;
79
+ status;
80
+ constructor(code, message, status = null) { super(message); this.name = 'OpenRouterLoginError'; this.code = code; this.status = status; }
81
+ }
82
+ const failure = (error) => error instanceof OpenRouterLoginError ? error : new OpenRouterLoginError(String(error?.code ?? 'OAUTH_FAILED'), 'The sign-in did not complete.', typeof error?.stato === 'number' ? error.stato : null);
83
+ const PAGE = (title) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>TALOS</title></head><body style="font-family:sans-serif;margin:3em"><h1>${title}</h1></body></html>`;
84
+ export async function startOpenRouterLogin({ oauth, env = process.env, paste = false, open = openInBrowser, fetchImpl = globalThis.fetch, timeoutMs = LOGIN_TIMEOUT_MS, label = LOGIN_LABEL }) {
85
+ const { verifier, challenge } = oauth.creaCoppiaPkce();
86
+ const state = oauth.creaStato();
87
+ const byHand = paste || isRemoteSession(env);
88
+ let settled = false, claimed = false, server = null, timer = null;
89
+ let resolveKey = () => { };
90
+ let rejectKey = () => { };
91
+ const key = new Promise((resolve, reject) => { resolveKey = resolve; rejectKey = reject; });
92
+ key.catch(() => { });
93
+ /* The port stops taking requests as soon as the sign-in is decided (RFC 8252 §8.3); the page the browser is waiting for is
94
+ still sent (a forced close there showed the person a broken connection instead of "Signed in"). Only a cancel or a
95
+ timeout drops the open connections too. */
96
+ const close = (force) => { if (timer) {
97
+ clearTimeout(timer);
98
+ timer = null;
99
+ } if (server) {
100
+ server.close();
101
+ if (force)
102
+ server.closeAllConnections?.();
103
+ server = null;
104
+ } };
105
+ const finish = (outcome, force = false) => { if (settled)
106
+ return; settled = true; close(force); if ('key' in outcome)
107
+ resolveKey(outcome.key);
108
+ else
109
+ rejectKey(outcome.error); };
110
+ const fetchDiRete = ((input, init) => fetchImpl(input, { ...init, signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS) }));
111
+ const exchange = async (code) => {
112
+ claimed = true;
113
+ try {
114
+ const { chiave } = await oauth.scambiaCodicePerChiave({ codice: code, verifier, fetchDiRete });
115
+ finish({ key: chiave });
116
+ }
117
+ catch (error) {
118
+ finish({ error: failure(error) });
119
+ }
120
+ return key;
121
+ };
122
+ let callbackUrl = null;
123
+ if (!byHand) {
124
+ const path = `/openrouter/callback/${encodeURIComponent(state)}`;
125
+ server = createServer((request, response) => {
126
+ const send = (status, title) => { response.statusCode = status; response.setHeader('content-type', 'text/html; charset=utf-8'); response.setHeader('cache-control', 'no-store'); response.setHeader('connection', 'close'); response.end(PAGE(title)); };
127
+ const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
128
+ if (request.method !== 'GET' || requestUrl.pathname !== path) {
129
+ send(404, 'Not found.');
130
+ return;
131
+ }
132
+ if (claimed || settled) {
133
+ send(409, 'This sign-in has already been used.');
134
+ return;
135
+ }
136
+ if (requestUrl.searchParams.get('error')) {
137
+ send(400, 'OpenRouter sign-in was cancelled. You can close this page.');
138
+ finish({ error: new OpenRouterLoginError('OAUTH_DENIED', 'OpenRouter sign-in was cancelled in the browser.') });
139
+ return;
140
+ }
141
+ const code = requestUrl.searchParams.get('code');
142
+ if (!code) {
143
+ send(400, 'OpenRouter returned no code.');
144
+ return;
145
+ }
146
+ void exchange(code).then(() => send(200, 'Signed in to OpenRouter. You can close this page and go back to TALOS.'), () => send(502, 'The sign-in did not complete. Go back to TALOS.'));
147
+ });
148
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', () => { server.removeListener('error', reject); resolve(); }); });
149
+ const address = server.address();
150
+ if (!address || typeof address === 'string') {
151
+ close(true);
152
+ throw new OpenRouterLoginError('OAUTH_CALLBACK_UNAVAILABLE', 'TALOS could not listen for OpenRouter on this machine.');
153
+ }
154
+ callbackUrl = `http://127.0.0.1:${address.port}${path}`;
155
+ }
156
+ const url = oauth.indirizzoDiAutorizzazione({ challenge, callbackUrl, etichetta: label });
157
+ timer = setTimeout(() => finish({ error: new OpenRouterLoginError('OAUTH_TIMEOUT', 'The sign-in took longer than 10 minutes.') }, true), timeoutMs);
158
+ timer.unref?.();
159
+ const opened = byHand ? false : open(url);
160
+ return {
161
+ url, mode: byHand ? 'paste' : 'browser', opened, key,
162
+ async submit(input) {
163
+ if (settled || claimed)
164
+ return key;
165
+ const code = codeFromInput(input);
166
+ if (!code)
167
+ throw new OpenRouterLoginError('OAUTH_CODE_MISSING', 'That is not a code from OpenRouter.');
168
+ return exchange(code);
169
+ },
170
+ cancel() { finish({ error: new OpenRouterLoginError('OAUTH_CANCELLED', 'The sign-in was cancelled.') }, true); },
171
+ };
172
+ }
@@ -0,0 +1,69 @@
1
+ /*
2
+ * P12 (owner, 2026-09-25, "quando riprendo sessione mi dà CTX_HISTORY_DIVERGED"). Measured on a copy of the owner's
3
+ * session and Context Engine archive: the archive held 10 messages, the last an assistant message with a tool call and NO
4
+ * result (12:16:45, the turn interrupted there); the session's own history had dropped that unanswered call, as it must (a
5
+ * call without its result is not sent back to a model), and gone on with the next person's messages. From then on the
6
+ * archive was no longer a prefix of the history, `syncOriginals` (harness-ui/src/context-desktop-service.mjs:202) refused
7
+ * every turn, and the session could not be resumed.
8
+ * Where the unanswered call came from: the kernel archives the whole history at several points of a turn, among them
9
+ * right after the model's answer, BEFORE the tools run (talosHarness.mjs `capture` reason 'response'). The cure, on the
10
+ * CLI's side of the bridge (the kernel and the service are not edited): `capture` never archives an assistant message
11
+ * whose tool calls do not all have a result yet, nor the partial results after it. The next capture, once the results
12
+ * are in, archives the exchange whole; if the turn stops first, nothing unanswered was archived. The archive stays a
13
+ * prefix of the history in both cases.
14
+ */
15
+ /** The longest prefix of `messages` that does not end inside an unanswered tool exchange. */
16
+ export function archivableHistory(messages) {
17
+ const rows = messages;
18
+ for (let index = rows.length - 1; index >= 0; index--) {
19
+ const message = rows[index];
20
+ if (message?.role !== 'assistant' || !Array.isArray(message.tool_calls) || message.tool_calls.length === 0)
21
+ continue;
22
+ const wanted = new Set(message.tool_calls.map((call) => call?.id).filter((id) => typeof id === 'string' && id));
23
+ for (let later = index + 1; later < rows.length && rows[later]?.role === 'tool'; later++)
24
+ wanted.delete(rows[later].tool_call_id);
25
+ /* The last tool exchange decides: complete, the whole history is archivable; unanswered, it and what follows wait. */
26
+ return wanted.size === 0 ? [...messages] : messages.slice(0, index);
27
+ }
28
+ return [...messages];
29
+ }
30
+ /*
31
+ * The same measurement found a race: `syncOriginals` reads the session revision, then appends with it
32
+ * (context-desktop-service.mjs:199-204), while a Context Engine compaction running in the background can commit a version
33
+ * in between (outside that session's queue). The append is then refused with CTX_STALE_REVISION and the TURN failed
34
+ * (r5a-coord-ce-compaction: 3 of 8 runs once capture waited for whole tool exchanges, 0 of 11 before; the race existed
35
+ * before too, only rarer). A retry is safe: `syncOriginals` rereads the revision and appends only what is missing, and
36
+ * `prepare` recomputes from the archive. So `capture` and `prepare` retry a stale revision, briefly.
37
+ * The shape follows Hermes' SQLite write retry (hermes_state.py:467-489, read 2026-09-26): patience is TIME-based, since
38
+ * "attempt-counted budgets destroyed turns on a healthy store", and each wait is RANDOM (20-150 ms), so two writers that
39
+ * lost the same race do not collide again in step (the "full jitter" of AWS's "Exponential Backoff and Jitter"). The
40
+ * budget is short, like Hermes' wait on a live compression lock: a compaction's commit is one write, and the revision reread
41
+ * after it succeeds; a revision that keeps moving for two seconds is a real conflict, and the error goes on.
42
+ */
43
+ const STALE_PATIENCE_MS = 2_000, STALE_WAIT_MIN_MS = 20, STALE_WAIT_MAX_MS = 150;
44
+ async function retryStale(operation, patienceMs) {
45
+ const deadline = Date.now() + patienceMs;
46
+ for (;;) {
47
+ try {
48
+ return await operation();
49
+ }
50
+ catch (error) {
51
+ const remaining = deadline - Date.now();
52
+ if (error?.code !== 'CTX_STALE_REVISION' || remaining <= 0)
53
+ throw error;
54
+ const wait = STALE_WAIT_MIN_MS + Math.random() * (STALE_WAIT_MAX_MS - STALE_WAIT_MIN_MS);
55
+ await new Promise(resolve => setTimeout(resolve, Math.min(wait, remaining)));
56
+ }
57
+ }
58
+ }
59
+ /** The service's kernel hooks: `capture` archives only whole tool exchanges; `capture` and `prepare` retry a stale revision. */
60
+ export function withArchivableCapture(hooks, { stalePatienceMs = STALE_PATIENCE_MS } = {}) {
61
+ if (!hooks || typeof hooks.capture !== 'function')
62
+ return hooks;
63
+ const capture = hooks.capture, prepare = hooks.prepare;
64
+ return {
65
+ ...hooks,
66
+ capture: (input) => retryStale(() => capture({ ...input, messages: Array.isArray(input?.messages) ? archivableHistory(input.messages) : input?.messages }), stalePatienceMs),
67
+ ...(typeof prepare === 'function' ? { prepare: (input) => retryStale(() => prepare(input), stalePatienceMs) } : {}),
68
+ };
69
+ }
@@ -89,7 +89,18 @@ export function findTalosRepoRoot(start = process.cwd(), deps = {}) {
89
89
  return cwd;
90
90
  throw new Error('TALOS_RUNTIME_NOT_FOUND');
91
91
  }
92
+ /* P18 phase 3 (owner 2026-09-26: ripgrep inside the npm package, no network): the kernel's `cerca` runs ripgrep from
93
+ TALOS_RG_PATH. `@vscode/ripgrep` ships the binary for this platform as an optional dependency of the package (no
94
+ install script, no download); a path already set, or a package without it, is left alone and `cerca` keeps its JS walk. */
95
+ let bundledRipgrep = null;
96
+ export function provideBundledRipgrep(env = process.env) {
97
+ if (env.TALOS_RG_PATH)
98
+ return Promise.resolve();
99
+ return bundledRipgrep ??= import('@vscode/ripgrep').then((m) => { if (typeof m?.rgPath === 'string' && existsSync(m.rgPath))
100
+ env.TALOS_RG_PATH = m.rgPath; }, () => { });
101
+ }
92
102
  export async function importTalosModule(repoRoot, relativeFromHarnessSrc) {
103
+ await provideBundledRipgrep();
93
104
  const file = join(repoRoot, 'harness-ui', 'src', relativeFromHarnessSrc);
94
105
  return import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
95
106
  }
@@ -158,9 +158,12 @@ export function createRuntimeSupervisor(options) {
158
158
  return;
159
159
  const sequence = nativeSequence(event);
160
160
  if (sequence !== null) {
161
- const added = replay.push(sessionId, event);
162
- if (!added || sequence <= cursor)
161
+ /* P13 (2026-09-26): what this subscriber has seen is ITS cursor. The buffer is shared by every subscriber of the
162
+ session, so "already in the buffer" dropped the whole history of a second subscriber (a reopened agent view came
163
+ up empty); it still records the event, and a different event at a known sequence still throws a conflict. */
164
+ if (sequence <= cursor)
163
165
  return;
166
+ replay.push(sessionId, event);
164
167
  cursor = sequence;
165
168
  }
166
169
  sink(event);
@@ -12,6 +12,8 @@ import { secretValuesFromEnvironment } from "../diagnostics/redact.js";
12
12
  import { isTestProfile } from "../provider/store.js";
13
13
  import { compactionEvent, createCliEventHub, createRunObserver, RETRY_AFTER_MAX_MS, summaryRequestedEvent } from "./provider-attempts.js";
14
14
  import { createToolOutputStore, pruneToolOutput } from "./output-store.js";
15
+ import { provideBundledRipgrep } from "./repo.js";
16
+ import { withArchivableCapture } from "./context-archive.js";
15
17
  import { CONTEXT_ENGINE_MIN_WINDOW, configureModelProfiles, createModelsDevLoader, modelProfile, modelProfileEvidence, ollamaWindow, rememberOllamaWindow, setModelOverrides } from "./model-profile.js";
16
18
  import { AsyncLocalStorage } from 'node:async_hooks';
17
19
  import { randomUUID } from 'node:crypto';
@@ -34,7 +36,7 @@ import { attachKeyOrigin, createEnvironmentKeyConsent, describeKeyOrigin, provid
34
36
  import { createProviderControlPlane } from "../provider/control-plane.js";
35
37
  function remapService(s) { return s.replace(/^talos-harness-/u, 'talos-cli-'); }
36
38
  function contextProfileValue(value) { return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : null; }
37
- function createCliContextBridge({ repoRoot, paths, registry, providerRegistry, providerStore, ownerRuntime, chatImageStore, separaFonteModello, prepareProfile, onSummaryRequest }) {
39
+ export function createCliContextBridge({ repoRoot, paths, registry, providerRegistry, providerStore, ownerRuntime, chatImageStore, separaFonteModello, prepareProfile, onSummaryRequest }) {
38
40
  /*
39
41
  * ⭐ R5a lane R, S2 (owner decision Q-R5-3): the profile — window and reserve — comes from `modelProfile` (model-profile.ts),
40
42
  * which layers the person's override, a local Ollama's own window, the provider's rows, the registry's documented rows and
@@ -215,8 +217,10 @@ function createCliContextBridge({ repoRoot, paths, registry, providerRegistry, p
215
217
  return service.request({ sessionId, method: 'GET', path: '/' });
216
218
  };
217
219
  return {
220
+ /* P12: `capture` archives only whole tool exchanges (context-archive.ts), so an interrupted turn cannot leave an
221
+ unanswered call in the archive that the history then drops (CTX_HISTORY_DIVERGED on every later turn). */
218
222
  async hooks(input) { await prepareForSession(input.sessionId); const profile = profileForSession(input.sessionId); if (!profile)
219
- return undefined; const { service } = await load(); await ensureSettings(service, input.sessionId); return service.createKernelHooks(input); },
223
+ return undefined; const { service } = await load(); await ensureSettings(service, input.sessionId); return withArchivableCapture(await service.createKernelHooks(input)); },
220
224
  async compact(input) { await prepareForSession(input.sessionId); const profile = profileForSession(input.sessionId); if (!profile)
221
225
  return undefined; const { service } = await load(); await ensureSettings(service, input.sessionId); return service.compact(input); },
222
226
  async status(sessionId) {
@@ -243,7 +247,7 @@ function createCliContextBridge({ repoRoot, paths, registry, providerRegistry, p
243
247
  export function scopeCliKeyring(raw) { if (!raw)
244
248
  return null; return { get: (s, a) => raw.get(remapService(s), a), set: (s, a, v) => raw.set(remapService(s), a, v), remove: (s, a) => raw.remove(remapService(s), a) }; }
245
249
  /** The kernel modules the product composes with. Exported so a test can observe one of them while every other stays the production one. */
246
- export async function loadModules(repoRoot) { const src = join(repoRoot, 'harness-ui', 'src'); const [cred, owner, agent, sessions, plugins, search, duck, providerRegistry, probe, destination, readiness, chatImages] = await Promise.all([import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-credential-store.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'runtime-owner-adapter.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'agent-service.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'session-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'plugin-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'search-source-store.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'duckduckgo-search.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-probe.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'model-destination.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'sessione-pronta.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'chat-image-attachments.mjs')).href))]); return { createProviderCredentialStore: cred.createProviderCredentialStore, createOwnerRuntimeAdapter: owner.createOwnerRuntimeAdapter, creaFetchMultiProvider: owner.creaFetchMultiProvider, compattaSessione: agent.compattaSessione, chiediAlModelloUnaVolta: agent.chiediAlModelloUnaVolta, avviaSessione: agent.avviaSessione, eseguiComandoDiretto: agent.eseguiComandoDiretto, createSessionRegistry: sessions.createSessionRegistry, verificaTrustPlugin: plugins.verificaTrustPlugin, createSearchSourceStore: search.createSearchSourceStore, creaTrasportoSenzaChiave: duck.creaTrasportoSenzaChiave, ENDPOINT_SENTINELLA_DUCKDUCKGO: duck.ENDPOINT_SENTINELLA_DUCKDUCKGO, REGISTRO_FORNITORI: providerRegistry.REGISTRO_FORNITORI, createProviderProbe: probe.createProviderProbe, separaFonteModello: destination.separaFonteModello, creaProntoFn: readiness.creaProntoFn, createChatImageStore: chatImages.createChatImageStore }; }
250
+ export async function loadModules(repoRoot) { await provideBundledRipgrep(); const src = join(repoRoot, 'harness-ui', 'src'); const [cred, owner, agent, sessions, plugins, search, duck, providerRegistry, probe, destination, readiness, chatImages] = await Promise.all([import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-credential-store.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'runtime-owner-adapter.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'agent-service.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'session-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'plugin-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'search-source-store.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'duckduckgo-search.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-probe.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'model-destination.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'sessione-pronta.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'chat-image-attachments.mjs')).href))]); return { createProviderCredentialStore: cred.createProviderCredentialStore, createOwnerRuntimeAdapter: owner.createOwnerRuntimeAdapter, creaFetchMultiProvider: owner.creaFetchMultiProvider, compattaSessione: agent.compattaSessione, chiediAlModelloUnaVolta: agent.chiediAlModelloUnaVolta, avviaSessione: agent.avviaSessione, eseguiComandoDiretto: agent.eseguiComandoDiretto, createSessionRegistry: sessions.createSessionRegistry, verificaTrustPlugin: plugins.verificaTrustPlugin, createSearchSourceStore: search.createSearchSourceStore, creaTrasportoSenzaChiave: duck.creaTrasportoSenzaChiave, ENDPOINT_SENTINELLA_DUCKDUCKGO: duck.ENDPOINT_SENTINELLA_DUCKDUCKGO, REGISTRO_FORNITORI: providerRegistry.REGISTRO_FORNITORI, createProviderProbe: probe.createProviderProbe, separaFonteModello: destination.separaFonteModello, creaProntoFn: readiness.creaProntoFn, createChatImageStore: chatImages.createChatImageStore }; }
247
251
  async function loadTrustSupport(repoRoot) {
248
252
  const src = join(repoRoot, 'harness-ui', 'src');
249
253
  const [plugins, hooks, mcp, mcpSession, pluginSession] = await Promise.all([import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'plugin-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'hook-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'mcp-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'mcp-session.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'plugin-session.mjs')).href))]);
@@ -962,6 +966,8 @@ export async function composeTalosRuntime({ repoRoot, projectRoot, paths, model,
962
966
  createProbe: storeProbe, environmentKey: (id) => environmentKeys === 'use' ? null : consent.stateFor(id),
963
967
  });
964
968
  tuiCatalog = createTuiCatalogService({
969
+ /* P21: OpenRouter sign-in with the kernel's PKCE module (the checkout's, or the package's vendor copy). */
970
+ openRouterLogin: async (input) => { const { loadOpenRouterOAuth, startOpenRouterLogin } = await import("../provider/openrouter-login.js"); return startOpenRouterLogin({ oauth: await loadOpenRouterOAuth(repoRoot), ...(input?.paste ? { paste: true } : {}) }); },
965
971
  publicProviders: () => { try {
966
972
  return providerStore.listPublic?.() ?? [];
967
973
  }
@@ -0,0 +1,51 @@
1
+ import { createTuiEventAdapter } from "./event-adapter.js";
2
+ import { createTranscriptModel, reduceTranscriptEvent } from "./transcript-model.js";
3
+ const TERMINAL = new Set(['run.completed', 'run.failed', 'run.cancelled']);
4
+ /** Folds a child's events into its view: the transcript, and whether its run is running or over. */
5
+ export function reduceAgentView(state, event) {
6
+ const transcript = reduceTranscriptEvent(state.transcript, event);
7
+ if (event.type === 'run.started')
8
+ return { ...state, transcript, running: true, done: false };
9
+ if (TERMINAL.has(event.type))
10
+ return { ...state, transcript, running: false, done: true };
11
+ return transcript === state.transcript ? state : { ...state, transcript };
12
+ }
13
+ export function createAgentView({ runtime, target, secretValues = [], onChange, batchMs = 16 }) {
14
+ const adapter = createTuiEventAdapter(secretValues);
15
+ adapter.bindSession(target.sessionId);
16
+ let state = { ...target, transcript: createTranscriptModel(), running: false, done: false };
17
+ let replaying = true, closed = false, timer = null;
18
+ /* A streamed answer is many small events: the screen is told at most once per frame. */
19
+ const notify = () => {
20
+ if (closed || replaying || timer)
21
+ return;
22
+ timer = setTimeout(() => { timer = null; if (!closed)
23
+ onChange(state); }, batchMs);
24
+ };
25
+ let off = () => { };
26
+ try {
27
+ off = runtime.subscribe(target.sessionId, raw => {
28
+ if (closed)
29
+ return;
30
+ for (const event of adapter.translateAll(raw))
31
+ state = reduceAgentView(state, replaying && event.type === 'run.started' ? { ...event, history: true } : event);
32
+ notify();
33
+ });
34
+ }
35
+ finally {
36
+ replaying = false;
37
+ }
38
+ if (!closed)
39
+ onChange(state);
40
+ return {
41
+ state: () => state,
42
+ close() { if (closed)
43
+ return; closed = true; if (timer) {
44
+ clearTimeout(timer);
45
+ timer = null;
46
+ } try {
47
+ off();
48
+ }
49
+ catch { /* nothing left to undo */ } },
50
+ };
51
+ }