mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-30 02:21:47 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05a773b426 | ||
|
|
d40b0d3c25 | ||
|
|
714e2397a4 | ||
|
|
c4aedfa9ed | ||
|
|
a0cf2b487c | ||
|
|
e3e2dcfe02 | ||
|
|
2433cf0130 | ||
|
|
1b21c41fa2 | ||
|
|
02049102b6 | ||
|
|
cfa13680e6 | ||
|
|
7856c664c3 | ||
|
|
7d8a354404 | ||
|
|
5d2ff1cb86 | ||
|
|
9aa899059f | ||
|
|
d33c0193cc | ||
|
|
e325742abf | ||
|
|
9ca70f3290 | ||
|
|
092438803b |
No files matched your search
@@ -4,7 +4,7 @@ import { getNotesSnapshot } from './notesStore';
|
||||
import { useActiveTabId } from './activeTabStore';
|
||||
import { getAppAppLockRuntime } from './appRuntimeBridge';
|
||||
import { createTrustedPluginAIHostRuntime } from '../../infrastructure/ai/plugin/trustedHostRuntime';
|
||||
import { getConfiguredPluginModel } from '../../infrastructure/ai/plugin/configuredModel';
|
||||
import { getConfiguredPluginModel, prepareConfiguredPluginModel } from '../../infrastructure/ai/plugin/configuredModel';
|
||||
import { buildSelectedPluginSources } from '../../infrastructure/ai/plugin/selectedSources';
|
||||
import { netcattyBridge } from '../../infrastructure/services/netcattyBridge';
|
||||
|
||||
@@ -29,6 +29,7 @@ export function usePluginAIContextHost({ sessions, workspaces }: { sessions: Ter
|
||||
const requests = new Map<string, { runtimeId: string; controller: AbortController }>();
|
||||
const runtime = createTrustedPluginAIHostRuntime({
|
||||
getModel: getConfiguredPluginModel,
|
||||
prepareModel: prepareConfiguredPluginModel,
|
||||
isActive: () => mounted && document.visibilityState === 'visible' && getAppAppLockRuntime()?.locked === false,
|
||||
chooseSelected: (signal, runtimeId) => new Promise((resolve, reject) => {
|
||||
if (chooser) { reject(Error('A host selection is already pending')); return; }
|
||||
|
||||
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
test('default host adapter, real preload, mounted host chooser and installed assistant reach only the local fixture', { timeout: 20_000 }, async () => {
|
||||
test('cold plugin host reaches the local fixture through the actual main provider allowlist without the AI sidebar', { timeout: 20_000 }, async (t) => {
|
||||
// JSDOM + the in-process runtime prove app wiring; they are not native sandbox
|
||||
// evidence. All state is synthetic, with no real provider, key, Note or host.
|
||||
const rootPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
@@ -42,6 +42,7 @@ test('default host adapter, real preload, mounted host chooser and installed ass
|
||||
const { inProcessBrowserRuntime } = require('../electron/plugins/fixtures/inProcessBrowserRuntime.cjs');
|
||||
const { createPreloadApi } = require('../electron/preload/api.cjs');
|
||||
const aiBridge = require('../electron/bridges/aiBridge.cjs');
|
||||
const { registerProviderHandlers } = require('../electron/bridges/aiBridge/providerHandlers.cjs');
|
||||
const { server, counts } = createAssistantProviderFixture({ delayMs: 25 });
|
||||
const oldNotes = getNotesSnapshot(); const oldLock = getAppAppLockRuntime();
|
||||
let service: ReturnType<typeof createPluginHostService> | undefined;
|
||||
@@ -51,6 +52,30 @@ test('default host adapter, real preload, mounted host chooser and installed ass
|
||||
const lockListeners = new Set<(state: { locked: boolean }) => void>();
|
||||
const reviews: Array<Record<string, unknown>> = [];
|
||||
const trustedPayloads: unknown[] = [];
|
||||
const streamResults: Array<{ ok: boolean; statusCode?: number; allowlistRejected: boolean }> = [];
|
||||
const providerSyncs: Array<{ count: number; selectedPresent: boolean }> = [];
|
||||
const sdkErrorNames: string[] = [];
|
||||
let diagnosticPhase = 'setup';
|
||||
const originalConsoleError = console.error;
|
||||
// node:sqlite was imported above. Node 22 delivers its ExperimentalWarning
|
||||
// asynchronously through console.error. Let module warnings reach normal
|
||||
// stderr before observing SDK/renderer errors; do not suppress that warning.
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
// The expected pre-fix SDK error contains request details. Keep diagnostics
|
||||
// categorical even on the failing-before path; never print payloads/headers.
|
||||
console.error = (value: unknown) => {
|
||||
let category = 'unknown';
|
||||
if (typeof value === 'string' && /not wrapped in act/.test(value)) category = 'react-act-warning';
|
||||
else if (typeof value === 'string' && /act.*environment|environment.*act/.test(value)) category = 'react-act-environment';
|
||||
else if (typeof value === 'string' && /ExperimentalWarning: SQLite is an experimental feature/.test(value)) category = 'node-sqlite-experimental';
|
||||
else if (typeof value === 'string') category = 'other-string';
|
||||
else if (value && typeof value === 'object') {
|
||||
const name = (value as { name?: unknown }).name;
|
||||
if (typeof name === 'string' && ['AbortError', 'AI_APICallError', 'AI_LoadAPIKeyError', 'TypeError', 'Error'].includes(name)) category = name;
|
||||
}
|
||||
sdkErrorNames.push(`${diagnosticPhase}:${category}`);
|
||||
};
|
||||
t.after(() => { console.error = originalConsoleError; });
|
||||
try {
|
||||
server.listen(0, '127.0.0.1'); await once(server, 'listening');
|
||||
const address = server.address(); assert.ok(address && typeof address !== 'string');
|
||||
@@ -62,25 +87,50 @@ test('default host adapter, real preload, mounted host chooser and installed ass
|
||||
ipcRenderer.emit(channel, {}, structuredClone(payload));
|
||||
} });
|
||||
const mainWindow = Object.assign(new EventEmitter(), { webContents: sender, isDestroyed: () => false, isVisible: () => true, isFocused: () => true });
|
||||
const providerContext = {
|
||||
ipcMain: { handle: (channel: string, handler: (...args: unknown[]) => unknown) => ipcHandlers.set(channel, handler) },
|
||||
providerConfigs: [] as Array<{ id: string; baseURL?: string; apiKey?: string }>, webSearchApiHost: null,
|
||||
validateSender: (event: { sender: unknown }) => event.sender === sender,
|
||||
validateSenderOrSettings: (event: { sender: unknown }) => event.sender === sender,
|
||||
// Only credential injection is synthetic. Admission, sync and the HTTP
|
||||
// transport are the production implementations, not a direct-fetch seam.
|
||||
injectApiKeyIntoRequest: (url: string, headers: Record<string, string>, providerId: string) => {
|
||||
const config = providerContext.providerConfigs.find((item) => item.id === providerId);
|
||||
return { url, headers: Object.fromEntries(Object.entries(headers).map(([key, value]) =>
|
||||
[key, config?.apiKey ? value.replace('__IPC_SECURED__', config.apiKey) : value])) };
|
||||
},
|
||||
shouldSkipTLSVerify: () => false,
|
||||
streamRequest: aiBridge._streamRequestForTests,
|
||||
};
|
||||
registerProviderHandlers(providerContext);
|
||||
assert.equal(providerContext.providerConfigs.length, 0);
|
||||
ipcRenderer.invoke = async (channel, payload) => {
|
||||
if (channel === 'netcatty:ai:chat:stream') {
|
||||
const request = payload as { url: string; headers: Record<string, string>; body: string; requestId: string };
|
||||
assert.equal(new URL(request.url).origin, new URL(endpoint).origin);
|
||||
assert.doesNotMatch(JSON.stringify(request), /fixture-only/);
|
||||
const status = await aiBridge._streamRequestForTests(request.url, { headers: request.headers, body: request.body }, { sender }, request.requestId, false);
|
||||
return { ok: status.statusCode === 200, ...status };
|
||||
}
|
||||
if (channel === 'netcatty:ai:chat:cancel') return aiBridge.cancelPluginAIStreamsForSender(sender) > 0;
|
||||
const handler = ipcHandlers.get(channel);
|
||||
assert.ok(handler, `Unexpected trusted IPC: ${channel}`);
|
||||
return handler({ sender }, structuredClone(payload));
|
||||
const result = await handler({ sender }, structuredClone(payload));
|
||||
if (channel === 'netcatty:ai:sync-providers') providerSyncs.push({ count: providerContext.providerConfigs.length,
|
||||
selectedPresent: providerContext.providerConfigs.some((item) => item.id === 'qa-provider') });
|
||||
if (channel === 'netcatty:ai:chat:stream') {
|
||||
const status = result as { ok: boolean; statusCode?: number; error?: string };
|
||||
streamResults.push({ ok: status.ok, statusCode: status.statusCode, allowlistRejected: status.error === 'URL host is not in the allowed list' });
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const api = createPreloadApi({ ipcRenderer, webUtils: {} });
|
||||
Object.defineProperty(dom.window, 'netcatty', { value: api });
|
||||
const syntheticNote = { id: 'synthetic-note', title: 'Synthetic retry plan', content: 'Synthetic retry plan: add a retry button.', createdAt: 1, updatedAt: 1 };
|
||||
publishNotesSnapshot({ notes: [syntheticNote], noteGroups: [] });
|
||||
registerAppAppLockRuntime({ locked: false } as NonNullable<ReturnType<typeof getAppAppLockRuntime>>);
|
||||
dom.window.localStorage.setItem(STORAGE_KEY_AI_PROVIDERS, JSON.stringify([{ id: 'qa-provider', name: 'QA fixture', providerId: 'custom', enabled: true, baseURL: endpoint, defaultModel: 'deterministic-fixture', apiKey: 'fixture-only' }]));
|
||||
dom.window.localStorage.setItem(STORAGE_KEY_AI_PROVIDERS, JSON.stringify([
|
||||
{ id: 'qa-provider', name: 'QA fixture', providerId: 'custom', enabled: true, baseURL: endpoint, defaultModel: 'deterministic-fixture', apiKey: 'fixture-only' },
|
||||
{ id: 'qa-secondary', name: 'Synthetic secondary', providerId: 'custom', enabled: true, baseURL: 'https://secondary.invalid/v1', defaultModel: 'synthetic', apiKey: 'secondary-fixture-marker' },
|
||||
]));
|
||||
dom.window.localStorage.setItem(STORAGE_KEY_AI_ACTIVE_PROVIDER, 'qa-provider');
|
||||
dom.window.localStorage.setItem(STORAGE_KEY_AI_ACTIVE_MODEL, 'deterministic-fixture');
|
||||
service = createPluginHostService({
|
||||
@@ -116,6 +166,7 @@ test('default host adapter, real preload, mounted host chooser and installed ass
|
||||
executeCommand: async (command: string, args: unknown) => structuredClone(await service!.contributionService.executeCommand(command, structuredClone(args))),
|
||||
onDidReceiveMessage: () => ({ dispose() {} }),
|
||||
} });
|
||||
diagnosticPhase = 'initial-selection';
|
||||
await act(async () => panel!.window.eval(fs.readFileSync(path.join(sample, 'panel.js'), 'utf8')));
|
||||
await waitFor(() => document.getElementById('plugin-ai-note') !== null);
|
||||
const noteChoice = document.getElementById('plugin-ai-note') as HTMLSelectElement;
|
||||
@@ -129,23 +180,33 @@ test('default host adapter, real preload, mounted host chooser and installed ass
|
||||
assert.equal(reference.value, ''); assert.equal(reference.options.length, 2);
|
||||
assert.notEqual(reference.options[1].value, syntheticNote.id); reference.value = reference.options[1].value;
|
||||
const send = () => element('compose').dispatchEvent(new panel!.window.Event('submit', { cancelable: true }));
|
||||
diagnosticPhase = 'deny-content';
|
||||
send(); await waitFor(() => element('status').textContent!.startsWith('Request ended'));
|
||||
assert.equal(counts.requests, 0); assert.equal(reviews.length, 0);
|
||||
contentAllowed = true;
|
||||
send(); await waitFor(() => element('status').textContent === 'Complete');
|
||||
diagnosticPhase = 'complete-stream';
|
||||
send(); await waitFor(() => counts.completed === 1 || streamResults.some((result) => !result.ok));
|
||||
assert.deepEqual(streamResults.at(-1), { ok: true, statusCode: 200, allowlistRejected: false },
|
||||
'Cold plugin startup must synchronize host providers before the main allowlist admits transport');
|
||||
assert.ok(providerSyncs.some((sync) => sync.selectedPresent && sync.count === 2), 'Preserve other host-configured providers in the shared registry');
|
||||
await waitFor(() => element('status').textContent === 'Complete');
|
||||
assert.equal(reviews.length, 0, 'content policy must not open a second native sharing review');
|
||||
assert.equal(counts.completed, 1); assert.match(element('response').textContent!, /Synthetic plan/); assert.match(element('usage').textContent!, /12 input tokens/);
|
||||
diagnosticPhase = 'stop-stream';
|
||||
send(); await waitFor(() => element('response').textContent!.startsWith('Synthetic plan: '));
|
||||
element('stop').dispatchEvent(new panel.window.Event('click'));
|
||||
await waitFor(() => element('status').textContent === 'Stopped' && counts.aborted === 1);
|
||||
const stoppedText = element('response').textContent; await new Promise((resolve) => setTimeout(resolve, 50)); assert.equal(element('response').textContent, stoppedText);
|
||||
diagnosticPhase = 'lock-stream';
|
||||
send(); await waitFor(() => element('response').textContent!.startsWith('Synthetic plan: '));
|
||||
locked = true; registerAppAppLockRuntime({ locked: true } as NonNullable<ReturnType<typeof getAppAppLockRuntime>>);
|
||||
for (const listener of lockListeners) listener({ locked: true });
|
||||
await waitFor(() => counts.aborted === 2 && service!.aiContextService.broker.turns.size === 0);
|
||||
assert.equal(aiBridge._getActiveStreamCountForTests(), 0);
|
||||
assert.doesNotMatch(JSON.stringify(trustedPayloads), /fixture-only|apiKey|privateOwner|permissionMode|mcp/);
|
||||
assert.doesNotMatch(JSON.stringify(trustedPayloads), /fixture-only|secondary-fixture-marker|apiKey|privateOwner|permissionMode|mcp/);
|
||||
assert.deepEqual(sdkErrorNames, []);
|
||||
} finally {
|
||||
console.error = originalConsoleError;
|
||||
panel?.window.close();
|
||||
await act(async () => root.unmount());
|
||||
await service?.manager.shutdown(); aiBridge.cleanup();
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Unified plugin acceptance candidate
|
||||
|
||||
This file records the frozen 677 native baseline. For the subsequent permission
|
||||
policy and AI candidate, use [the new integration record](policy-ai-integration.md)
|
||||
and its package manifest. Historical consent flows below are not the revised UX.
|
||||
|
||||
The dedicated branch is `integration/plugin-native-acceptance`. Its Draft PR
|
||||
records the exact current head and CI runs. Native AI, database, Host/Vault, and
|
||||
settings/logging checks use this candidate rather than separate feature trees.
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Plugin model transport cold start
|
||||
|
||||
Commit `092438803bed96753bfe68cb78da9ca1137dee25` removes an independent startup
|
||||
dependency on the internal AI sidebar. It does not establish the cause or outcome
|
||||
of the original native failure; that still requires an authorized native retest.
|
||||
|
||||
## Reproduced boundary
|
||||
|
||||
At native candidate `677aac33b1eec9dbd4a367417a49c964cdea63e7`, the plugin model
|
||||
selector reads the saved host provider, while main's provider registry starts
|
||||
empty. The existing sidebar synchronizes that registry when visible or sending.
|
||||
The plugin startup path did not. A nondefault configured loopback port can
|
||||
therefore fail main's provider allowlist before HTTP transport starts.
|
||||
|
||||
The revised `components/PluginAIContextHost.test.tsx` installs the browser sample,
|
||||
mounts the trusted host component, uses the real application preload, and routes
|
||||
model requests through `registerProviderHandlers` and its actual allowlist. It
|
||||
does not mount the internal sidebar or manually alter the allowlist. Credential
|
||||
injection uses synthetic fixture markers; the HTTP edge and deterministic local
|
||||
fixture use the existing transport. No paid service, real key, Note, or host is
|
||||
used. This remains DOM/in-process evidence, not native sandbox acceptance.
|
||||
|
||||
Failed-before evidence is `/tmp/netcatty-ai-cold-provider-before-final.log`:
|
||||
|
||||
```text
|
||||
ok: false
|
||||
allowlistRejected: true
|
||||
statusCode: undefined
|
||||
```
|
||||
|
||||
The corrected regression passes and preserves both the selected fixture provider
|
||||
and a second host-configured synthetic provider. It checks that provider markers
|
||||
and configuration do not enter plugin-facing model/context envelopes. Expected
|
||||
SDK error output is suppressed during this diagnostic so request payloads and
|
||||
headers are not logged.
|
||||
|
||||
## Correction
|
||||
|
||||
Before publishing a selected model handle, the trusted renderer synchronizes the
|
||||
current host-owned provider settings through the existing `aiSyncProviders`
|
||||
application API. It checks that the selected model revision matches the snapshot
|
||||
being synchronized. The runtime rechecks activity, cancellation, model revision,
|
||||
and pending-selection ownership after the await. Pending selections count toward
|
||||
the model-handle cap; release and shutdown invalidate late results.
|
||||
|
||||
Plugin requests supply no URL, credential, or provider configuration. The existing
|
||||
main sender validation, endpoint allowlist, source binding, and model invocation
|
||||
budgets are unchanged. No new schema feature, plugin permission, or consent
|
||||
dialog is added. The assistant package is unchanged and retains SHA-256
|
||||
`c7d784d5c3bd2feb463afd58eae3a9594615395b168174cb8a3d0677584ae395`.
|
||||
|
||||
Production integration touches `configuredModel.ts`, `trustedHostRuntime.ts`, and
|
||||
`usePluginAIContextHost.ts`. The hook's import and `prepareModel` option may
|
||||
overlap host-selection work; preserve both when integrating the separate policy
|
||||
task. Permission-engine, consent-dialog, and sample-UI changes are outside this
|
||||
correction.
|
||||
|
||||
## Native and policy status
|
||||
|
||||
The native open/close sidebar comparison was incomplete. Execution auto-review
|
||||
denied the Application-scope `context.selected.list` grant; that action did not
|
||||
execute. No post-sidebar inference was performed, the prompt expired, and the
|
||||
sample was disabled while the worker cleaned up. This is not native confirmation
|
||||
of the startup cause. The original `677aac33` app remains frozen for comparison;
|
||||
this fix does not authorize retrying the denied execution action.
|
||||
|
||||
In the earlier permission engine, **Application** grants are memory-only for the
|
||||
application lifetime; **Always** grants are persisted. An explicitly authorized
|
||||
Application grant can fit bounded temporary QA. A one-use prompt on every stream
|
||||
read is not an unavoidable target UX. The separate policy task implements the
|
||||
new direction: default Allow for declared supported capabilities, per-plugin
|
||||
Allow/Ask/Deny settings, audit, and in-app Ask dialogs. That product change is
|
||||
separate from execution-tool authorization and must not be used to evade a tool
|
||||
denial. This transport correction does not redesign that policy.
|
||||
|
||||
Native outcome, independent verification, and the sample's light-theme appearance
|
||||
follow-up remain separate acceptance work. No GUI launch, sandbox bypass, paid
|
||||
model invocation, main merge, or release was performed for this correction.
|
||||
|
||||
## Node 22 CI observer correction
|
||||
|
||||
The new cold-start test failed on exact head `9ca70f329` in test run
|
||||
`40436198585`, Linux job `121965743101`: 15,206 passed, 1 failed, 50 skipped.
|
||||
The sole failure was the empty SDK/renderer-error assertion receiving one
|
||||
`SDK_OR_RENDERER_ERROR` marker. This red result is retained as evidence; it is
|
||||
not an unrelated baseline failure.
|
||||
|
||||
The CI runtime was Node `22.23.3`; local validation had used Node `24.19.0`.
|
||||
Reproduction on the exact CI version identified the safe category
|
||||
`setup:node-sqlite-experimental`, before any model operation. The existing
|
||||
`node:sqlite` import schedules its ExperimentalWarning on the next tick via
|
||||
`console.error`. The newly added observer was installed before that warning
|
||||
drained. The same test passed on Node 24, which did not emit that warning.
|
||||
|
||||
The test now lets pending module warnings reach normal stderr before installing
|
||||
the SDK/renderer observer. The warning remains visible, and the zero-runtime-error
|
||||
assertion is unchanged. Safe phase/error categories make future failures
|
||||
diagnosable without request payloads or headers. No production behavior, timeout,
|
||||
permission, or cancellation assertion changed. Both Node versions pass the
|
||||
focused test; the Node 22 parallel fixture/UI/provider/preload group passes all
|
||||
16 tests with concurrency 4.
|
||||
|
||||
Diagnostic evidence: `/tmp/netcatty-ai-cold-ci-node22-sqlite-category.log` fails
|
||||
before correction, `/tmp/netcatty-ai-cold-ci-node22-after.log` passes with the
|
||||
SQLite warning still visible, and `/tmp/netcatty-ai-cold-ci-node22-parallel.log`
|
||||
records the parallel check. The appearance-only sample change is independently
|
||||
published as `e325742abf9fd329f1f7858eda66367e7a62357c`; its package hash is
|
||||
`7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274`.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Managed-window Ask acceptance candidate
|
||||
|
||||
The dedicated branch is `integration/plugin-ask-routing-acceptance`. Its Draft
|
||||
PR records the exact frozen integration head, CI and private renderer artifact
|
||||
handoff. It starts from frozen #4001 at
|
||||
`02049102b6732778adcd0f33b0a0ade6b4752327`; the old source checkout, QA workspace,
|
||||
profile and renderer artifact remain unchanged.
|
||||
|
||||
## Exact inputs and ownership
|
||||
|
||||
The only new input is policy owner's reviewed Ask correction
|
||||
`89beeb93749c751f255beb6d0e0f95d23bdd9a7b`, imported by ordinary merge
|
||||
`1b21c41fa26c0173084c7ae75e640ef3274285dd`. There were no merge conflicts or
|
||||
manual production edits. These seven production files exactly match that input:
|
||||
|
||||
| File | Owned production delta |
|
||||
| --- | --- |
|
||||
| `electron/bridges/windowManager.cjs` | Enumerate managed app windows, including Settings |
|
||||
| `electron/main/registerBridges.cjs` | Supply the managed permission-window getter |
|
||||
| `electron/plugins/hostService.cjs` | Pass that getter to the existing decision host |
|
||||
| `electron/plugins/permissionDecisionHost.cjs` | Bind ready/focused/visible/unlocked managed renderer selection and request lifecycle to the chosen receiver |
|
||||
| `electron/plugins/pluginBridge.cjs` | Guard permission readiness/replies by managed app window and main frame; reject plugin/foreign/subframe senders |
|
||||
| `components/SettingsPage.tsx` | Mount the existing permission host for standalone Settings |
|
||||
| `components/plugins/PluginPermissionHost.tsx` | Mark Ask with the existing host-security surface so attached native views yield |
|
||||
|
||||
The input also owns the Settings-installed-entrypoint regression, renderer test
|
||||
updates and policy/security documentation. AI d33 startup wiring, sample assets,
|
||||
the combined cold-start assertions and all earlier policy/transport history are
|
||||
preserved from 020. `ModelSelector.tsx` is unchanged from 020; the separate
|
||||
model-selector failure-loop repair is not imported. Diagnostic #3999 remains
|
||||
excluded. No broker resource/source rules, stream restrictions, package/lock
|
||||
inputs, native dependencies or sandbox configuration change in this integration.
|
||||
|
||||
## Frozen 020 native evidence
|
||||
|
||||
The parent closed that QA session and released the GUI. Its partial outcome is:
|
||||
|
||||
| Native case on 020 | Result |
|
||||
| --- | --- |
|
||||
| Default Allow cold Send and completion | Passed |
|
||||
| Stop, AI Deny, restore without automatic replay | Passed |
|
||||
| Themes in new views | Passed |
|
||||
| Ask visible dialog | Failed: no visible dialog, timeout, zero new model requests |
|
||||
| Ask acceptance/cancel and content-read matrix | Not run; remain pending |
|
||||
|
||||
The input's independent source-only delta review is scoped clean. Its installed
|
||||
SDK/registered IPC test reproduces the old Settings-owned failure and passes with
|
||||
the correction using represented Electron objects. That is not native Ask
|
||||
success. No new native acceptance is claimed by this coding executor.
|
||||
|
||||
Earlier terminal solo/split core passes with corrected fixtures, unreliable/
|
||||
unverified Focus and one QA Enter in A during a palette attempt remain historical
|
||||
evidence. There is no global zero-input or full terminal native-pass claim. The
|
||||
old Rfam run failed socket transport before connect resolved, without reaching
|
||||
authentication/SELECT; its cause and auth/TLS compatibility remain unverified.
|
||||
The next native priority is the new Ask path, not more old-baseline loops.
|
||||
|
||||
## Checks and retained CI history
|
||||
|
||||
| Local check | Result |
|
||||
| --- | --- |
|
||||
| Registered runtime | 696 tests: 695 passed, one native skip, zero failed/cancelled |
|
||||
| Contract/SDK/CLI | 91 passed |
|
||||
| Offline examples | Five passed, one Docker opt-in skip |
|
||||
| Mounted cold-start AI/permission/selected-summary/Host group | 21 passed |
|
||||
| Settings and security-surface visibility group | 37 passed |
|
||||
| Contract/capability/Codex generation and drift | Clean |
|
||||
| Lint | Zero errors, seven existing warnings |
|
||||
| Five sample archives | Validated; two builds each are identical and match frozen 020 |
|
||||
|
||||
The exact integration production build, renderer inventory and final CI are
|
||||
recorded in the Draft PR and artifact provenance. Local native packaging and
|
||||
unpackaged GUI acceptance are not claimed. This environment uses locked
|
||||
dependencies and standard patches; the official pinned Electron binary uses its
|
||||
documented cache/proxy support. No header workaround, native shim, HOME change
|
||||
or sandbox change is introduced.
|
||||
|
||||
Input89 [test CI 40453446775](https://github.com/binaricat/Netcatty/actions/runs/40453446775)
|
||||
succeeded on the parent's single infrastructure-only rerun: 15,342 passed, 50
|
||||
skipped, zero failed/cancelled. The original runner interruption remains evidence;
|
||||
this worker did not duplicate its retry. Input89
|
||||
[four-platform packages](https://github.com/binaricat/Netcatty/actions/runs/40453446781)
|
||||
passed with release jobs skipped. Frozen 020's successful full/platform CI and
|
||||
all earlier diagnosed/unexplained failures remain separate predecessor evidence.
|
||||
|
||||
The [sample manifest](ask-routing-packages.json) records exact IDs, versions,
|
||||
features, permissions/resources and archive/content hashes. All five archives
|
||||
are byte-identical to 020, including Assistant
|
||||
`7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274`.
|
||||
No archive was activated by this executor. Its unchanged owner README retains
|
||||
old review wording; the current Allow/Ask/Deny procedure governs acceptance.
|
||||
|
||||
## Production-renderer preparation and native Ask recipe
|
||||
|
||||
1. Create a new isolated checkout at this Draft's exact head and a separate
|
||||
disposable synthetic QA profile. Keep the old 020 workspace/profile intact.
|
||||
2. Match `package.json` and `package-lock.json` hashes to the renderer artifact's
|
||||
provenance and the independently normally rebuilt native dependencies. They
|
||||
are unchanged from 020. Prepare matching host workspace packages normally.
|
||||
**The complete backend checkout must match the new head:** the frontend-only
|
||||
bundle does not include the new CJS window-routing code or native binaries.
|
||||
3. Materialize the exact private Library file/version in the consumer's own
|
||||
environment. Verify its archive SHA-256 and every dist file with the supplied
|
||||
inventory/verification script. Producer-local paths are not portable.
|
||||
4. Use those verified production assets as a whole `dist/` in the matching
|
||||
checkout, with the ordinary Electron entrypoint, no Vite environment and
|
||||
normal sandbox/helper settings. This is native **unpackaged production-
|
||||
renderer QA**, not packaged-binary certification. Process-loss causality
|
||||
remains unclassified; no native build/header workaround is supplied.
|
||||
5. Recreate only the synthetic provider/Note/sample setup. Verify Default Allow
|
||||
remains functional; then set the plugin operation to Ask in the active main
|
||||
or standalone Settings window. Keep the selected receiver visible, focused,
|
||||
ready and unlocked; opening Ask must not force focus back to main.
|
||||
6. Record visible readable Ask and native-view yielding. Check cancellation and
|
||||
acceptance as separate explicit operations: cancel must send no model request;
|
||||
accept must authorize only the current bounded operation. Complete the
|
||||
content-read Allow/Ask/Deny matrix and lifecycle invalidation cases. Do not
|
||||
treat the earlier zero-request timeout as an Ask pass.
|
||||
7. Record exact head, archive/sample hashes, request counters, prompt ownership,
|
||||
screenshots/video and any unrun cases. Report a model-selector failure
|
||||
separately rather than folding its other owner's repair into this candidate.
|
||||
|
||||
Hosting/distribution/deployment and remaining DB/terminal acceptance gates stay
|
||||
parent-coordinated. No main merge, Ready transition or release is part of this
|
||||
handoff.
|
||||
@@ -0,0 +1,197 @@
|
||||
[
|
||||
{
|
||||
"name": "assistant",
|
||||
"id": "com.netcatty.assistant",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 6,
|
||||
"bytes": 17214,
|
||||
"sha256": "7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274",
|
||||
"contentSha256": "c3e31e9613422bf9a05ccaa5b6f234304208f1a8298b1c1ee19262ba88af6cd0",
|
||||
"features": {
|
||||
"required": [
|
||||
"ai.model-invocation",
|
||||
"context.selected-read"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"views"
|
||||
],
|
||||
"optional": [
|
||||
"ai.invoke",
|
||||
"context.selected.list",
|
||||
"context.selected.read"
|
||||
]
|
||||
},
|
||||
"package": "assistant.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "c7d784d5c3bd2feb463afd58eae3a9594615395b168174cb8a3d0677584ae395",
|
||||
"changedEntriesFromFrozen677": [
|
||||
"README.md",
|
||||
"panel.css",
|
||||
"panel.js"
|
||||
],
|
||||
"previous7856Sha256": "7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "89beeb93749c751f255beb6d0e0f95d23bdd9a7b",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274",
|
||||
"changedEntriesFrom020": []
|
||||
},
|
||||
{
|
||||
"name": "database-owned",
|
||||
"id": "com.netcatty.database-minimal",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 5,
|
||||
"bytes": 11660,
|
||||
"sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"contentSha256": "4d628c41c7a231e31cd56dacb822acaaa37f61e1e98b285b0086a4f783be199d",
|
||||
"features": {
|
||||
"required": [
|
||||
"network.byte-streams"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"views"
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"permission": "network.stream",
|
||||
"resources": [
|
||||
"tcp://10.77.0.20:3306"
|
||||
],
|
||||
"reason": "Connect only to the prepared disposable fixture."
|
||||
}
|
||||
]
|
||||
},
|
||||
"package": "database-owned.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "89beeb93749c751f255beb6d0e0f95d23bdd9a7b",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"changedEntriesFrom020": []
|
||||
},
|
||||
{
|
||||
"name": "database-rfam-readonly",
|
||||
"id": "com.netcatty.database-rfam-readonly",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 5,
|
||||
"bytes": 11003,
|
||||
"sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"contentSha256": "f542ca7b8e821d684cf7f71f0930458bd3ad97bc94bf654c88b0f353111e9162",
|
||||
"features": {
|
||||
"required": [
|
||||
"network.byte-streams"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"views"
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"permission": "network.stream",
|
||||
"resources": [
|
||||
"tcp://mysql-rfam-public.ebi.ac.uk:4497"
|
||||
],
|
||||
"reason": "Run the fixed public Rfam acceptance query only after explicit authorization."
|
||||
}
|
||||
]
|
||||
},
|
||||
"package": "database-rfam-readonly.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "89beeb93749c751f255beb6d0e0f95d23bdd9a7b",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"changedEntriesFrom020": []
|
||||
},
|
||||
{
|
||||
"name": "host-actions",
|
||||
"id": "com.netcatty.host-actions",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 3,
|
||||
"bytes": 4633,
|
||||
"sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"contentSha256": "d6f8bf3ae9dc417ea838f673966f2e1075fc8f9ef1d939ca85b476b1bb007e6c",
|
||||
"features": {
|
||||
"required": [
|
||||
"vault.host-actions",
|
||||
"commands.interactive"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"menus"
|
||||
],
|
||||
"optional": [
|
||||
"vault.metadata",
|
||||
"vault.write",
|
||||
"ui.interact"
|
||||
]
|
||||
},
|
||||
"package": "host-actions.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "89beeb93749c751f255beb6d0e0f95d23bdd9a7b",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"changedEntriesFrom020": []
|
||||
},
|
||||
{
|
||||
"name": "terminal-selection-draft",
|
||||
"id": "com.netcatty.terminal-selection-draft",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 2,
|
||||
"bytes": 2954,
|
||||
"sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"contentSha256": "99797d8679f34b96580076f922cbec067e511adc0f4ccf396ed129206acb6c4c",
|
||||
"features": {
|
||||
"required": [
|
||||
"terminal.selection-sharing",
|
||||
"terminal.input-drafts",
|
||||
"commands.interactive"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"menus"
|
||||
],
|
||||
"optional": [
|
||||
"terminal.selection.read",
|
||||
"terminal.input.propose"
|
||||
]
|
||||
},
|
||||
"package": "terminal-selection-draft.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "89beeb93749c751f255beb6d0e0f95d23bdd9a7b",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"changedEntriesFrom020": []
|
||||
}
|
||||
]
|
||||
@@ -1,5 +1,10 @@
|
||||
# Test the selected Note assistant on an isolated desktop
|
||||
|
||||
For the permission-policy/AI acceptance candidate, first use
|
||||
[its integration record](policy-ai-integration.md) and exact package manifest.
|
||||
The older permission-dialog sequence below is historical baseline evidence;
|
||||
apply the current Allow/Ask/Deny semantics when testing the new candidate.
|
||||
|
||||
This recipe tests a sandbox plugin against a deterministic loopback provider.
|
||||
The provider does not run a model or contact a paid service. The profile contains
|
||||
only synthetic data. The parent owns native screenshots and video on the working
|
||||
@@ -29,9 +34,9 @@ browser runtime entry. It has no Node entry, networking, filesystem, secrets,
|
||||
storage, terminal, or command-execution permission. Its `commands` permission
|
||||
registers its own view callbacks. It cannot execute terminal commands.
|
||||
The six-file artifact at this checkpoint has SHA-256
|
||||
`c7d784d5c3bd2feb463afd58eae3a9594615395b168174cb8a3d0677584ae395`.
|
||||
This replaces the earlier `c0f73369...` artifact after the selected-Note expiry
|
||||
recovery repair.
|
||||
`7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274`.
|
||||
This replaces the earlier `c7d784d5...` artifact with the sample-only appearance
|
||||
update. The host provider cold-start correction remains a separate app change.
|
||||
|
||||
## Start the deterministic fixture
|
||||
|
||||
@@ -119,6 +124,11 @@ Synthetic retry plan: add a retry button and show a connection status label.
|
||||
in the dropdown, and click Send yourself. Deny the fresh content permission
|
||||
once and cancel text review once before allowing a successful fixture run.
|
||||
The draft must survive all steps, and no request may be silently retried.
|
||||
14. With the sample idle, switch the host between light and dark and change its
|
||||
accent. The page, controls, muted surfaces, text, focus ring, and Send button
|
||||
must follow the existing view environment tokens without model requests or
|
||||
new permission prompts. Capture both appearances on the same host window.
|
||||
This check does not change or bypass the separately owned permission policy.
|
||||
|
||||
The expiry regression in `scripts/plugin-assistant-panel.test.cjs` controls the
|
||||
panel and actual host service/selected-context authority clocks and advances past
|
||||
|
||||
@@ -450,6 +450,11 @@ new child to an opened parent directory without a path race.
|
||||
`readDirectory()` likewise keeps its stable SDK/RPC method but fails closed
|
||||
unless the main process supplies a native adapter whose inode checks and entry
|
||||
enumeration are bound to the same directory handle.
|
||||
The default host supplies no `openPluginDirectoryHandle` adapter. The SDK exposes
|
||||
the method and the router registers it, but there is no separate advertised
|
||||
feature flag that enables enumeration. Even with an allowed `filesystem.read`
|
||||
directory resource, the default runtime returns `unsupported`. Do not interpret
|
||||
the method's presence or a filesystem permission as directory-listing support.
|
||||
|
||||
Permission names already use the phase-3 enforcement boundaries: clipboard
|
||||
read/write, terminal metadata/output/input and input/output interception, Vault
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# Permission-policy and AI acceptance candidate
|
||||
|
||||
This records frozen 020 and its predecessor evidence. For the later managed-
|
||||
window Ask correction, use [the isolated Ask integration record](ask-routing-integration.md).
|
||||
020 native Default Allow/Stop/Deny/restore/themes passed; Ask had no visible
|
||||
dialog and timed out with zero new model requests. Ask acceptance/cancel and
|
||||
the content-read matrix remain unrun until that new candidate is checked.
|
||||
|
||||
Use the dedicated `integration/plugin-policy-ai-acceptance` branch. Its Draft PR
|
||||
records the exact published head and aggregate CI. Frozen #3997 at
|
||||
`677aac33b1eec9dbd4a367417a49c964cdea63e7` remains the historical native baseline;
|
||||
neither that branch nor main is changed by this integration.
|
||||
|
||||
This is a candidate for parent-coordinated acceptance. It is not a claim of
|
||||
completed native acceptance, deployment readiness or permission activation.
|
||||
|
||||
## Preserved inputs
|
||||
|
||||
| Input | Exact checkpoint | Integration |
|
||||
| --- | --- | --- |
|
||||
| Frozen integration | `677aac33b1eec9dbd4a367417a49c964cdea63e7` | Starting point; includes stream/source/Host/settings and prior terminal history |
|
||||
| Permission policy #4000 | `79a9bd6df1c17fd8a5238aba31d7a8b4cda184b9` | Initial ordinary merge `9aa899059` preserved 2eee561; follow-up ordinary merge `cfa13680e` preserves the authoritative scope/sensitivity copy and tests |
|
||||
| AI #3986 | `d33c0193ccaedc1a10aed9da495f040250121bef` | Ordinary merge `5d2ff1cb8`; cold-start wiring, themed sample and Node 22 warning-drain test correction |
|
||||
| Terminal production | #3996 `8e7c28fe1bdbcdd76816d33ec926ba8e60eefe64` | Preserve the prior renderer eligibility/publication path; policy-owned interaction changes remain from #4000 |
|
||||
|
||||
Diagnostic #3999 and its tests/docs-only `3fdaa357` continuation are not merged.
|
||||
No terminal runtime fix is required by the fixture-classification finding.
|
||||
Database protocol and fixed SQL remain solely in the minimal examples.
|
||||
|
||||
## Direct integration decisions
|
||||
|
||||
The initial merge conflict was `components/PluginAIContextHost.test.tsx`. The
|
||||
combined test keeps policy's absence of a second native content-sharing review
|
||||
and AI's actual main allowlist, two-provider synchronization, streamed result,
|
||||
Stop/lock cleanup and empty SDK-error assertion. The Node 22 warning drain is
|
||||
retained. The obsolete forced-review cancellation branch is superseded by the
|
||||
current in-app Ask/cancellation tests; no transport assertion is dropped.
|
||||
|
||||
No production file needed a manual conflict resolution. Policy production files
|
||||
remain identical to the policy input; the three AI startup wiring files and
|
||||
sample assets remain identical to d33c0193. The stream service/router and prior
|
||||
terminal renderer eligibility/publication files are unchanged from frozen 677.
|
||||
|
||||
Integration commit `7d8a354404d68071d0924532ffe74c713ace6cb9` updates two stale
|
||||
disclosure assertions in `PluginPackageManager.test.tsx` and
|
||||
`PluginMarketplace.interaction.test.tsx`. They expected the old words
|
||||
`not a grant`; the new assertions require default Allow, Ask/Deny controls and
|
||||
retained host validation/resource scope. Existing escaping, advanced-authority,
|
||||
source and README-failure assertions remain intact. Production copy is unchanged.
|
||||
|
||||
The later authoritative 79a9bd6 policy merge overlapped those two tests. Their
|
||||
final contents are byte-identical to 79a9bd6: all three earlier policy assertions
|
||||
remain, together with the owner's additional unbounded-resource, sensitivity,
|
||||
escaping/source-order and advanced-authority checks. No assertion was dropped.
|
||||
This follow-up imports the owner's five-locale wording changes; it makes no
|
||||
manual production conflict edit and leaves the shared AI test intact.
|
||||
|
||||
## Checks and retained failures
|
||||
|
||||
| Local check | Result |
|
||||
| --- | --- |
|
||||
| Registered runtime | 695 tests: 694 passed, one native skip, zero failed/cancelled |
|
||||
| Contract/SDK/CLI | 91 passed |
|
||||
| Offline examples | Five passed, one explicit Docker opt-in skip |
|
||||
| Serial mounted AI/permission/selected-summary/Host/provider/panel group at 7856 | 31 passed |
|
||||
| Final settings, terminal interaction, cold-start host and locale group | 90 passed |
|
||||
| Contract/capability/Codex generation and drift | Passed |
|
||||
| Full lint | Zero errors, seven existing warnings |
|
||||
| Production build | Passed; Vite 35.50 seconds |
|
||||
| Five sample archives | Validated; repeat builds byte-identical |
|
||||
|
||||
The initial settings group records 34 passes and two failures on the stale copy
|
||||
assertions. That run is retained separately from the corrected 36/36 result.
|
||||
|
||||
Policy input [test CI 40443396430](https://github.com/binaricat/Netcatty/actions/runs/40443396430)
|
||||
first received an explicit runner shutdown. The parent requested one retry;
|
||||
this integration task did not duplicate it. Retry Linux job `121992430185`
|
||||
passed registered runtime (694/one skip) and examples (five/one skip), then
|
||||
failed those same two `not a grant` assertions: settings group 33 passed/two
|
||||
failed. It did not reach the full suite. The candidate includes the isolated
|
||||
test correction above; do not relabel the input CI as passing. Both input Windows
|
||||
jobs and [policy package CI](https://github.com/binaricat/Netcatty/actions/runs/40443396402)
|
||||
passed. Parent reports the independent policy correction reviews scoped clean,
|
||||
including both P2s and the three UI findings, with 71 focused/synthetic checks.
|
||||
|
||||
The policy owner subsequently corrected these tests in predecessor
|
||||
`da4e80946d2f3fb48f205aae2a732252b2f51fca`. Its
|
||||
[test CI](https://github.com/binaricat/Netcatty/actions/runs/40444934631) passed
|
||||
15,342 cases with 50 skipped and no failures/cancellations, and its
|
||||
[four-platform package CI](https://github.com/binaricat/Netcatty/actions/runs/40444934738)
|
||||
passed. The final 79a9bd6 wording head's
|
||||
[test CI](https://github.com/binaricat/Netcatty/actions/runs/40446529092) passed;
|
||||
its [package workflow](https://github.com/binaricat/Netcatty/actions/runs/40446529100)
|
||||
is tracked separately in the Draft PR. No input retry was duplicated here.
|
||||
|
||||
Integration predecessor `7856c664c3c2255a21badef2758bb29311b27a01` passed its
|
||||
[test CI](https://github.com/binaricat/Netcatty/actions/runs/40445809589): 15,344
|
||||
passed, 50 skipped, zero failed/cancelled, plus both Windows jobs and native
|
||||
automated checks. Its [four-platform packages](https://github.com/binaricat/Netcatty/actions/runs/40445809590)
|
||||
also passed. These are retained predecessor results; the new exact head needs
|
||||
its own aggregate CI.
|
||||
|
||||
AI input [test CI](https://github.com/binaricat/Netcatty/actions/runs/40439111241)
|
||||
and [package CI](https://github.com/binaricat/Netcatty/actions/runs/40439111215)
|
||||
passed. Parent reports its three isolated changes scoped clean. Its prior 9ca
|
||||
Node 22 warning-observer failure remains documented in `ai-provider-startup.md`;
|
||||
the warning drain does not ignore errors or remove the empty-error assertion.
|
||||
Frozen 677's earlier unexplained MCP assertion remains separate history.
|
||||
|
||||
This executor used locked dependencies with install scripts disabled, then the
|
||||
standard repository dependency patches. The official pinned Electron binary
|
||||
installed through its documented proxy support and executes in Node mode.
|
||||
No native-header download, alternate mirror, native shim, HOME change or
|
||||
sandbox change was used. Normal native postinstall/rebuild, local full-suite
|
||||
success and local desktop packaging are not claimed. Exact candidate CI is the
|
||||
aggregate full-suite and platform-packaging verification surface.
|
||||
|
||||
## Exact packages and provenance
|
||||
|
||||
The machine-readable [package record](policy-ai-packages.json) contains complete
|
||||
IDs, versions, required features, permission/resource declarations, archive and
|
||||
content hashes, and frozen-baseline comparisons. All five versions are 0.1.0.
|
||||
|
||||
| Sample | SHA-256 | Comparison with frozen 677 |
|
||||
| --- | --- | --- |
|
||||
| Assistant | `7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274` | Matches the AI owner's new themed archive; only README.md, panel.css and panel.js differ from 677 |
|
||||
| Owned database | `f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874` | Byte-identical |
|
||||
| Rfam | `aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3` | Byte-identical |
|
||||
| Host actions | `cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2` | Byte-identical |
|
||||
| Terminal draft | `4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e` | Byte-identical |
|
||||
|
||||
After the authoritative 79 follow-up, all five archives were rebuilt twice,
|
||||
validated and compared byte for byte with the 7856 candidate: all five remain
|
||||
identical. No sample permission/resource declaration changed. The host's new policy changes
|
||||
effective authorization behavior; identical package bytes do not certify that
|
||||
new host behavior. The old Assistant c7d784d5 archive remains historical evidence,
|
||||
not the themed archive. No handoff archive was activated by this coding executor.
|
||||
The owner-identical Assistant README retains earlier permission-review wording;
|
||||
use this candidate's Allow/Ask/Deny procedure below for acceptance. That packaged
|
||||
copy is a documentation limitation, not a requirement for repeated dialogs.
|
||||
|
||||
## Native preparation and remaining gates
|
||||
|
||||
Use the exact candidate head from its Draft PR in a separate disposable native
|
||||
checkout/profile. Keep normal platform sandbox/helper settings. Build and hash:
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
npm run build
|
||||
node examples/plugins/assistant/build.mjs
|
||||
node packages/plugin-cli/dist/cli.js pack examples/plugins/assistant/.package --out /tmp/assistant.ncpkg
|
||||
node examples/plugins/database-minimal/prepare.mjs tcp://10.77.0.20:3306 /tmp/database-owned.ncpkg
|
||||
node examples/plugins/database-rfam-readonly/prepare.mjs tcp://mysql-rfam-public.ebi.ac.uk:4497 /tmp/database-rfam-readonly.ncpkg
|
||||
node packages/plugin-cli/dist/cli.js pack examples/plugins/host-actions --out /tmp/host-actions.ncpkg
|
||||
node examples/plugins/terminal-selection-draft/prepare.mjs /tmp/terminal-selection-draft.ncpkg
|
||||
```
|
||||
|
||||
Validate each archive with the plugin CLI and compare against the machine record.
|
||||
The owned DB address is packaging-only. A different approved fixture endpoint
|
||||
requires a new archive and hash. Preparation does not dial either endpoint.
|
||||
|
||||
- Verify default Allow for declared supported capabilities without a mandatory
|
||||
first-install permission checklist or repeated permission-only dialogs. Verify
|
||||
per-plugin Ask/Deny, one scoped Ask receipt, readable host-selected labels,
|
||||
lifecycle invalidation and redacted audit. Preserve necessary target selection
|
||||
and inert draft editing, exact resources, source isolation and sandboxing.
|
||||
- Verify the assistant's cold startup through the normal host without opening
|
||||
the AI sidebar. Native inference on frozen 677 returned unavailable; the
|
||||
independent startup regression did not establish that native error's cause.
|
||||
Use only the deterministic synthetic provider and Note recipe, with current
|
||||
policy semantics. Native success is not claimed before that recheck.
|
||||
- Frozen 677 Host core passed per parent; old consent recordings describe the
|
||||
prior policy. They do not certify the new permission implementation.
|
||||
- Terminal's source-level finding is fixture-sensitive-input fail-closed:
|
||||
`fixture-A> ` is untrusted; `fixture@owned-A:~$ ` is confirmed shell. Parent
|
||||
reports corrected-fixture solo/split core passed on unmodified 677. Focus
|
||||
remained unreliable/unverified, and QA sent one Enter in A during a palette
|
||||
attempt. Do not claim global zero input or full terminal native acceptance.
|
||||
Keep the inert draft's no-PTY-dispatch property distinct from that QA action.
|
||||
Exclude diagnostic instrumentation. Prior-policy results do not certify the
|
||||
new policy; the next native priority is final-policy and cold-start AI QA.
|
||||
- Native Rfam's single baseline run failed with sanitized -32014 socket error
|
||||
before connect resolved. Auth/SELECT were not reached; transport causality and
|
||||
auth/TLS compatibility remain unverified. No probe/fallback or restriction
|
||||
change is authorized by this coding task. Owned DB fixture prerequisites and
|
||||
parent coordination remain separate.
|
||||
- Record exact head, hashes, ordered prompt/operation outcomes, screenshots and
|
||||
video for the new candidate. Hosting/distribution/deployment remain pending;
|
||||
no main merge, Ready transition or release is part of this handoff.
|
||||
@@ -0,0 +1,187 @@
|
||||
[
|
||||
{
|
||||
"name": "assistant",
|
||||
"id": "com.netcatty.assistant",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 6,
|
||||
"bytes": 17214,
|
||||
"sha256": "7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274",
|
||||
"contentSha256": "c3e31e9613422bf9a05ccaa5b6f234304208f1a8298b1c1ee19262ba88af6cd0",
|
||||
"features": {
|
||||
"required": [
|
||||
"ai.model-invocation",
|
||||
"context.selected-read"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"views"
|
||||
],
|
||||
"optional": [
|
||||
"ai.invoke",
|
||||
"context.selected.list",
|
||||
"context.selected.read"
|
||||
]
|
||||
},
|
||||
"package": "assistant.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "c7d784d5c3bd2feb463afd58eae3a9594615395b168174cb8a3d0677584ae395",
|
||||
"changedEntriesFromFrozen677": [
|
||||
"README.md",
|
||||
"panel.css",
|
||||
"panel.js"
|
||||
],
|
||||
"previous7856Sha256": "7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "79a9bd6df1c17fd8a5238aba31d7a8b4cda184b9",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef"
|
||||
},
|
||||
{
|
||||
"name": "database-owned",
|
||||
"id": "com.netcatty.database-minimal",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 5,
|
||||
"bytes": 11660,
|
||||
"sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"contentSha256": "4d628c41c7a231e31cd56dacb822acaaa37f61e1e98b285b0086a4f783be199d",
|
||||
"features": {
|
||||
"required": [
|
||||
"network.byte-streams"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"views"
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"permission": "network.stream",
|
||||
"resources": [
|
||||
"tcp://10.77.0.20:3306"
|
||||
],
|
||||
"reason": "Connect only to the prepared disposable fixture."
|
||||
}
|
||||
]
|
||||
},
|
||||
"package": "database-owned.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "79a9bd6df1c17fd8a5238aba31d7a8b4cda184b9",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef"
|
||||
},
|
||||
{
|
||||
"name": "database-rfam-readonly",
|
||||
"id": "com.netcatty.database-rfam-readonly",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 5,
|
||||
"bytes": 11003,
|
||||
"sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"contentSha256": "f542ca7b8e821d684cf7f71f0930458bd3ad97bc94bf654c88b0f353111e9162",
|
||||
"features": {
|
||||
"required": [
|
||||
"network.byte-streams"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"views"
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"permission": "network.stream",
|
||||
"resources": [
|
||||
"tcp://mysql-rfam-public.ebi.ac.uk:4497"
|
||||
],
|
||||
"reason": "Run the fixed public Rfam acceptance query only after explicit authorization."
|
||||
}
|
||||
]
|
||||
},
|
||||
"package": "database-rfam-readonly.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "79a9bd6df1c17fd8a5238aba31d7a8b4cda184b9",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef"
|
||||
},
|
||||
{
|
||||
"name": "host-actions",
|
||||
"id": "com.netcatty.host-actions",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 3,
|
||||
"bytes": 4633,
|
||||
"sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"contentSha256": "d6f8bf3ae9dc417ea838f673966f2e1075fc8f9ef1d939ca85b476b1bb007e6c",
|
||||
"features": {
|
||||
"required": [
|
||||
"vault.host-actions",
|
||||
"commands.interactive"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"menus"
|
||||
],
|
||||
"optional": [
|
||||
"vault.metadata",
|
||||
"vault.write",
|
||||
"ui.interact"
|
||||
]
|
||||
},
|
||||
"package": "host-actions.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "79a9bd6df1c17fd8a5238aba31d7a8b4cda184b9",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef"
|
||||
},
|
||||
{
|
||||
"name": "terminal-selection-draft",
|
||||
"id": "com.netcatty.terminal-selection-draft",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 2,
|
||||
"bytes": 2954,
|
||||
"sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"contentSha256": "99797d8679f34b96580076f922cbec067e511adc0f4ccf396ed129206acb6c4c",
|
||||
"features": {
|
||||
"required": [
|
||||
"terminal.selection-sharing",
|
||||
"terminal.input-drafts",
|
||||
"commands.interactive"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"menus"
|
||||
],
|
||||
"optional": [
|
||||
"terminal.selection.read",
|
||||
"terminal.input.propose"
|
||||
]
|
||||
},
|
||||
"package": "terminal-selection-draft.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "79a9bd6df1c17fd8a5238aba31d7a8b4cda184b9",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,140 @@
|
||||
# Settings visibility and current-main acceptance candidate
|
||||
|
||||
This isolated branch, `integration/plugin-settings-main-acceptance`, starts at
|
||||
frozen #4004 `2433cf0130d1c3274c56c39e5765349c2945de01`. Its Draft PR and private
|
||||
renderer provenance record the final exact head and CI. Frozen2433 source,
|
||||
artifact, QA workspace/profile and earlier evidence remain intact.
|
||||
|
||||
## Inputs and production ownership
|
||||
|
||||
Both inputs were ordinarily merged, without conflicts or manual production edits:
|
||||
|
||||
| Input | Import | Production files and behavior |
|
||||
| --- | --- | --- |
|
||||
| Current main `a0cf2b487c9ff0b0472d622e66bc5ee20de32c71` | Merge `c4aedfa9ed8c9f2647ac26f0e6e144edab341c2d` | `domain/workspaceAiScopeHandoff.ts` keeps inherited AI chat on a surviving owner when a workspace dissolves (#4003/e3e2dcfe); `electron/bridges/ai/shellUtils.cjs` narrowly recognizes Oh My Zsh ys two-line idle prompts (#4008/a0cf2b48) |
|
||||
| Policy `1551fa8355021b5a811d737d16f81e8070f99f5d` | Merge `714e2397a43dc6c083dce8afeeb7a0475de11384` | `PluginLogSettings.tsx` removes the idle-row security marker and opts the nested verbose confirmation into it; `confirm-dialog.tsx` adds the optional host-only marker with default false |
|
||||
|
||||
All four production files exactly match their respective owners. Main has only
|
||||
those two new commits since common base f0897c3c. Neither changed file overlaps
|
||||
the prior plugin changes. The new prompt utility is not imported by plugin
|
||||
production modules; plugin sensitive-input, selection and source guards remain
|
||||
unchanged. Existing prompt and live-PTY-byte suffix tests still reject bare
|
||||
markers and pending input. These are synthetic tests, not a new native SSH pass.
|
||||
|
||||
The input also owns its real Settings landing regression and documentation.
|
||||
The existing observer, Ask marker, IPC/window authority, sandbox, resource/source
|
||||
controls, AI d33 wiring, schemas, sample bytes and package/lock inputs are
|
||||
unchanged. `ModelSelector.tsx` equals2433: separate #4002 is not included. Global
|
||||
Agent/custom Chat #4006 and terminal diagnostic #3999 are excluded. No main merge,
|
||||
Ready transition, original-PR closure or release is part of this handoff.
|
||||
|
||||
The parent reports the1551-vs89 independent delta review scoped clean: idle
|
||||
Settings no longer requests suspension, the real logging dialog remains marked,
|
||||
its nested verbose portal protects itself while the parent is accessibility-
|
||||
hidden, closing the nested dialog leaves the parent protected, and closing all
|
||||
dialogs permits restoration. This source-only result is not native resolution
|
||||
of the recorded Settings blank body.
|
||||
|
||||
## Frozen2433 native results
|
||||
|
||||
The parent supplied these results; this coding executor did not run the GUI:
|
||||
|
||||
| Case | Result on2433 |
|
||||
| --- | --- |
|
||||
| Main Ask visible | Passed |
|
||||
| Cancel | Passed; zero provider POSTs |
|
||||
| Accept | Passed; one provider POST |
|
||||
| Stream chunks | Passed; no repeated permission prompts |
|
||||
| Content Deny | Passed |
|
||||
| Real five-minute chat expiry | Passed |
|
||||
| Settings sample body | Blank; not accepted |
|
||||
|
||||
The idle-row suppression defect is source-confirmed. The correction requires a
|
||||
new native Settings recheck; no native success is inferred from the DOM regression.
|
||||
A later GPU exit9 and incomplete cleanup were reported separately. Its cause is
|
||||
unclassified; no source hotpatch or sandbox/GPU workaround was used here.
|
||||
|
||||
Consumer verification of2433's Library renderer is complete: the parent checked
|
||||
the matching full source/backend and all1001 renderer hashes, with native inputs
|
||||
unchanged. The earlier producer download failure remains historical evidence,
|
||||
not an outstanding consumer transfer blocker. That verified artifact still
|
||||
certifies2433 only; it does not include1551 or the new main renderer behavior.
|
||||
|
||||
Earlier020 Default Allow cold Send/completion, Stop/Deny/restore and themes,
|
||||
677 Host core, and corrected-fixture terminal solo/split results remain scoped
|
||||
historical evidence. Terminal Focus is unreliable/unverified and QA sent one
|
||||
Enter inA during a palette attempt: no global zero-input/full-terminal-pass claim.
|
||||
The historical Rfam attempt failed socket transport before connect resolved;
|
||||
authentication/SELECT were not reached and cause/auth/TLS compatibility remain
|
||||
unverified. Final source-policy, DB, placement/lifecycle and actual hosting/CDN
|
||||
gates remain separately reconciled by the parent.
|
||||
|
||||
## Local checks and sample provenance
|
||||
|
||||
| Check | Result |
|
||||
| --- | --- |
|
||||
| Registered plugin runtime | 695 passed, one native skip; zero failed/cancelled |
|
||||
| Contract/SDK/CLI | 91 passed |
|
||||
| Offline examples | Five passed, one Docker opt-in skip |
|
||||
| Changed main owner/scope/prompt/suffix and terminal lifecycle group | 139 passed |
|
||||
| Actual mounted AI/permission/selected-context/Host group | 21 passed |
|
||||
| Settings boundary and security visibility group | 38 passed |
|
||||
| Contract/capability/Codex generation and committed drift | Clean |
|
||||
| Lint | Zero errors, seven existing warnings |
|
||||
| Five sample archives | Validated, built twice and byte-identical to2433 |
|
||||
|
||||
The final exact-head normal production build, renderer inventory, bounded
|
||||
credential check, CI and Codex review are recorded in the Draft PR/artifact
|
||||
handoff. Local full-suite, native packaging and GUI success are not claimed.
|
||||
Locked dependencies, standard patches and the official pinned Electron binary
|
||||
were prepared normally; no header, HOME, sandbox or native shim workaround.
|
||||
|
||||
[Exact sample IDs, permissions/resources and hashes](settings-main-packages.json)
|
||||
preserve all prior provenance, including Assistant7943d4c3, ownedDBf01c6a24,
|
||||
Rfamaaca5866, Hostcf04f82d and terminal4c9c2388. Whole archive equality was
|
||||
asserted, rather than inferred from unchanged source. The owned10.77.0.20 endpoint
|
||||
is packaging-only, not a running fixture. No package was activated or service
|
||||
queried by this executor.
|
||||
|
||||
The owner's separate QA-only Settings Ask sample has reported SHA-256
|
||||
`9502a58a4804862e4b13de9fc0d1baf6ad8c2bc681c08b5c0664b552ef0aafc4`.
|
||||
It is not one of these five repository examples and was not rebuilt here. The
|
||||
native owner must retain its exact verified manifest/source/permissions and
|
||||
fixture provenance; no package ID, permission expansion or hash equivalence is
|
||||
invented by this integration.
|
||||
|
||||
## Native preparation and bounded recheck
|
||||
|
||||
1. After the parent's GUI/Nginx slot is released, use a NEW isolated checkout of
|
||||
the final exact head and a separate synthetic QA profile. Preserve the old2433
|
||||
workspace/profile and recordings. Materialize the new private Library artifact
|
||||
in the consumer's own workspace and verify archive and every renderer hash.
|
||||
2. Use the complete matching backend checkout: frontend-only dist does not carry
|
||||
the CJS prompt helper or native dependencies. Match package/lock hashes to
|
||||
independently normally rebuilt native inputs and prepare workspace packages
|
||||
normally. Install dist as a whole; use the ordinary Electron launch without
|
||||
Vite and with normal sandbox/helper settings. This is unpackaged native
|
||||
production-renderer QA, not packaged-binary certification.
|
||||
3. Open the verified Settings sample through its existing supported settings
|
||||
entry. Check the idle body is visible/clickable with the logging row present.
|
||||
Open logging and the nested verbose confirmation separately; native content
|
||||
must yield to each, remain hidden after cancelling the nested confirmation,
|
||||
and restore only after the parent closes. Do not enable real verbose logging
|
||||
or collect real content for this check.
|
||||
4. In the ready/focused/visible/unlocked Settings owner, exercise its synthetic
|
||||
own-setting Ask operation. Record visible Ask, explicit cancel/accept, exact
|
||||
current operation outcome and restoration. Recheck main Ask briefly. Preserve
|
||||
the2433 main/content/expiry counters and any unrun matrix cases separately.
|
||||
5. Use owned synthetic A/B AI chats: form a workspace, prefer B, dissolve while A
|
||||
survives and confirm both histories stay with their owners. Remove A and
|
||||
check bounded fallback to B. Confirm handoff alone does not invoke a model or
|
||||
re-share content. Exercise selected-target invalidation across the transition
|
||||
with inert drafts and no PTY input. Do not treat earlier Focus uncertainty as
|
||||
resolved by this main domain fix.
|
||||
6. Record exact source/artifact/sample identities, screenshots/video, request
|
||||
counters, cleanup and unrun cases. No fallback endpoint/probe, real credentials,
|
||||
paid API, security relaxation or unrecognized installation is authorized here.
|
||||
|
||||
If main advances after this pinned a0cf snapshot, compare the new delta and assess
|
||||
its affected checks before landing. The current candidate deliberately does not
|
||||
chase unrelated changes while native preparation is in progress.
|
||||
@@ -0,0 +1,217 @@
|
||||
[
|
||||
{
|
||||
"name": "assistant",
|
||||
"id": "com.netcatty.assistant",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 6,
|
||||
"bytes": 17214,
|
||||
"sha256": "7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274",
|
||||
"contentSha256": "c3e31e9613422bf9a05ccaa5b6f234304208f1a8298b1c1ee19262ba88af6cd0",
|
||||
"features": {
|
||||
"required": [
|
||||
"ai.model-invocation",
|
||||
"context.selected-read"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"views"
|
||||
],
|
||||
"optional": [
|
||||
"ai.invoke",
|
||||
"context.selected.list",
|
||||
"context.selected.read"
|
||||
]
|
||||
},
|
||||
"package": "assistant.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "c7d784d5c3bd2feb463afd58eae3a9594615395b168174cb8a3d0677584ae395",
|
||||
"changedEntriesFromFrozen677": [
|
||||
"README.md",
|
||||
"panel.css",
|
||||
"panel.js"
|
||||
],
|
||||
"previous7856Sha256": "7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "1551fa8355021b5a811d737d16f81e8070f99f5d",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274",
|
||||
"changedEntriesFrom020": [],
|
||||
"rebuiltAgainstMain": "a0cf2b487c9ff0b0472d622e66bc5ee20de32c71",
|
||||
"productionMerge": "714e2397a43dc6c083dce8afeeb7a0475de11384",
|
||||
"previous2433Sha256": "7943d4c3800914fe0acd3d0f3a3d7f7c1910c212a5dc58a044871a6a82485274",
|
||||
"changedEntriesFrom2433": []
|
||||
},
|
||||
{
|
||||
"name": "database-owned",
|
||||
"id": "com.netcatty.database-minimal",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 5,
|
||||
"bytes": 11660,
|
||||
"sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"contentSha256": "4d628c41c7a231e31cd56dacb822acaaa37f61e1e98b285b0086a4f783be199d",
|
||||
"features": {
|
||||
"required": [
|
||||
"network.byte-streams"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"views"
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"permission": "network.stream",
|
||||
"resources": [
|
||||
"tcp://10.77.0.20:3306"
|
||||
],
|
||||
"reason": "Connect only to the prepared disposable fixture."
|
||||
}
|
||||
]
|
||||
},
|
||||
"package": "database-owned.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "1551fa8355021b5a811d737d16f81e8070f99f5d",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"changedEntriesFrom020": [],
|
||||
"rebuiltAgainstMain": "a0cf2b487c9ff0b0472d622e66bc5ee20de32c71",
|
||||
"productionMerge": "714e2397a43dc6c083dce8afeeb7a0475de11384",
|
||||
"previous2433Sha256": "f01c6a24382897d4b0bf4130941274abacb9a9a963f9b56ecaceb2c020f36874",
|
||||
"changedEntriesFrom2433": []
|
||||
},
|
||||
{
|
||||
"name": "database-rfam-readonly",
|
||||
"id": "com.netcatty.database-rfam-readonly",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 5,
|
||||
"bytes": 11003,
|
||||
"sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"contentSha256": "f542ca7b8e821d684cf7f71f0930458bd3ad97bc94bf654c88b0f353111e9162",
|
||||
"features": {
|
||||
"required": [
|
||||
"network.byte-streams"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"views"
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"permission": "network.stream",
|
||||
"resources": [
|
||||
"tcp://mysql-rfam-public.ebi.ac.uk:4497"
|
||||
],
|
||||
"reason": "Run the fixed public Rfam acceptance query only after explicit authorization."
|
||||
}
|
||||
]
|
||||
},
|
||||
"package": "database-rfam-readonly.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "1551fa8355021b5a811d737d16f81e8070f99f5d",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"changedEntriesFrom020": [],
|
||||
"rebuiltAgainstMain": "a0cf2b487c9ff0b0472d622e66bc5ee20de32c71",
|
||||
"productionMerge": "714e2397a43dc6c083dce8afeeb7a0475de11384",
|
||||
"previous2433Sha256": "aaca586669db454e91d61487e576ed524d1b7190eed182bc2cd0035e59caa0d3",
|
||||
"changedEntriesFrom2433": []
|
||||
},
|
||||
{
|
||||
"name": "host-actions",
|
||||
"id": "com.netcatty.host-actions",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 3,
|
||||
"bytes": 4633,
|
||||
"sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"contentSha256": "d6f8bf3ae9dc417ea838f673966f2e1075fc8f9ef1d939ca85b476b1bb007e6c",
|
||||
"features": {
|
||||
"required": [
|
||||
"vault.host-actions",
|
||||
"commands.interactive"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"menus"
|
||||
],
|
||||
"optional": [
|
||||
"vault.metadata",
|
||||
"vault.write",
|
||||
"ui.interact"
|
||||
]
|
||||
},
|
||||
"package": "host-actions.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "1551fa8355021b5a811d737d16f81e8070f99f5d",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"changedEntriesFrom020": [],
|
||||
"rebuiltAgainstMain": "a0cf2b487c9ff0b0472d622e66bc5ee20de32c71",
|
||||
"productionMerge": "714e2397a43dc6c083dce8afeeb7a0475de11384",
|
||||
"previous2433Sha256": "cf04f82dfe25ab5cda6f5f3dad0a8fb06c37bedf9924f556bee2b8402e542ef2",
|
||||
"changedEntriesFrom2433": []
|
||||
},
|
||||
{
|
||||
"name": "terminal-selection-draft",
|
||||
"id": "com.netcatty.terminal-selection-draft",
|
||||
"version": "0.1.0",
|
||||
"fileCount": 2,
|
||||
"bytes": 2954,
|
||||
"sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"contentSha256": "99797d8679f34b96580076f922cbec067e511adc0f4ccf396ed129206acb6c4c",
|
||||
"features": {
|
||||
"required": [
|
||||
"terminal.selection-sharing",
|
||||
"terminal.input-drafts",
|
||||
"commands.interactive"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"required": [
|
||||
"commands",
|
||||
"menus"
|
||||
],
|
||||
"optional": [
|
||||
"terminal.selection.read",
|
||||
"terminal.input.propose"
|
||||
]
|
||||
},
|
||||
"package": "terminal-selection-draft.ncpkg",
|
||||
"repeatIdentical": true,
|
||||
"preparedOnlyInThisExecutor": true,
|
||||
"previousFrozen677Sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"changedEntriesFromFrozen677": [],
|
||||
"previous7856Sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"changedEntriesFrom7856": [],
|
||||
"rebuiltAgainstPolicy": "1551fa8355021b5a811d737d16f81e8070f99f5d",
|
||||
"rebuiltAgainstAI": "d33c0193ccaedc1a10aed9da495f040250121bef",
|
||||
"previous020Sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"changedEntriesFrom020": [],
|
||||
"rebuiltAgainstMain": "a0cf2b487c9ff0b0472d622e66bc5ee20de32c71",
|
||||
"productionMerge": "714e2397a43dc6c083dce8afeeb7a0475de11384",
|
||||
"previous2433Sha256": "4c9c2388b7f891156192a0b5ab7b23291beba3555e7ec8cf24fbe2dfe1242d5e",
|
||||
"changedEntriesFrom2433": []
|
||||
}
|
||||
]
|
||||
@@ -108,7 +108,7 @@ test('handoffDissolvedWorkspaceAIScope retargets inherited chat when its pane is
|
||||
});
|
||||
});
|
||||
|
||||
test('handoffDissolvedWorkspaceAIScope retargets inherited chat when original pane also survives', () => {
|
||||
test('handoffDissolvedWorkspaceAIScope keeps inherited chat on its surviving owner pane', () => {
|
||||
const result = handoffDissolvedWorkspaceAIScope({
|
||||
activeSessionIdMap: {
|
||||
'workspace:ws-1': 'chat-a',
|
||||
@@ -132,12 +132,52 @@ test('handoffDissolvedWorkspaceAIScope retargets inherited chat when original pa
|
||||
preferredTerminalId: 'term-b',
|
||||
});
|
||||
|
||||
assert.equal(result.changed, true);
|
||||
assert.equal(result.activeSessionIdMap['terminal:term-b'], 'chat-a');
|
||||
assert.equal(result.activeSessionIdMap['terminal:term-a'], null);
|
||||
assert.equal(result.sessions[0]?.scope.targetId, 'term-b');
|
||||
assert.equal(result.activeSessionIdMap['terminal:term-a'], 'chat-a');
|
||||
assert.equal(result.activeSessionIdMap['terminal:term-b'], 'chat-b');
|
||||
assert.equal(result.sessions[0]?.scope.type, 'terminal');
|
||||
assert.equal(result.sessions[0]?.scope.targetId, 'term-a');
|
||||
assert.equal(result.sessions[0]?.updatedAt, 1);
|
||||
assert.equal(result.sessions[1]?.scope.targetId, 'term-b');
|
||||
assert.deepEqual(result.panelViewByScope['terminal:term-b'], {
|
||||
assert.deepEqual(result.panelViewByScope['terminal:term-a'], {
|
||||
mode: 'session',
|
||||
sessionId: 'chat-a',
|
||||
});
|
||||
assert.equal(result.panelViewByScope['terminal:term-b'], undefined);
|
||||
});
|
||||
|
||||
test('handoffDissolvedWorkspaceAIScope keeps owner history when the other pane is preferred', () => {
|
||||
const result = handoffDissolvedWorkspaceAIScope({
|
||||
activeSessionIdMap: {
|
||||
'workspace:ws-1': 'chat-a',
|
||||
'terminal:term-b': 'chat-b',
|
||||
},
|
||||
sessions: [
|
||||
{
|
||||
id: 'chat-a',
|
||||
scope: { type: 'terminal', targetId: 'term-a', hostIds: ['host-a'] },
|
||||
updatedAt: 1,
|
||||
},
|
||||
{
|
||||
id: 'chat-b',
|
||||
scope: { type: 'terminal', targetId: 'term-b', hostIds: ['host-b'] },
|
||||
updatedAt: 1,
|
||||
},
|
||||
],
|
||||
workspaceId: 'ws-1',
|
||||
terminalIds: ['term-a', 'term-b'],
|
||||
preferredTerminalId: 'term-b',
|
||||
panelViewByScope: {
|
||||
'workspace:ws-1': { mode: 'session', sessionId: 'chat-a' },
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.changed, true);
|
||||
assert.equal(result.activeSessionIdMap['terminal:term-a'], 'chat-a');
|
||||
assert.equal(result.activeSessionIdMap['terminal:term-b'], 'chat-b');
|
||||
assert.equal(result.sessions[0]?.scope.targetId, 'term-a');
|
||||
assert.equal(result.sessions[1]?.id, 'chat-b');
|
||||
assert.equal(result.sessions[1]?.scope.targetId, 'term-b');
|
||||
assert.deepEqual(result.panelViewByScope['terminal:term-a'], {
|
||||
mode: 'session',
|
||||
sessionId: 'chat-a',
|
||||
});
|
||||
|
||||
@@ -78,13 +78,15 @@ export function seedWorkspaceAIActiveSessionFromMembers(input: {
|
||||
}
|
||||
|
||||
/**
|
||||
* When a workspace tab dissolves, copy its active chat onto the preferred
|
||||
* surviving terminal before the workspace scope key is pruned. Also remint
|
||||
* workspace-scoped chats and the handed-off active chat when it still lives
|
||||
* under a different terminal (including one that also survives dissolve),
|
||||
* clear that previous terminal selection so ownership filters do not hide the
|
||||
* chat, and seed a session panel view so terminal scopes do not fall back to
|
||||
* a blank draft.
|
||||
* When a workspace tab dissolves, copy its active chat onto a surviving
|
||||
* terminal before the workspace scope key is pruned. An inherited chat that
|
||||
* is already terminal-scoped to a pane still in `terminalIds` stays on that
|
||||
* owner, even when focus/preferred is a different survivor. Otherwise the
|
||||
* target is the preferred survivor, or the first remaining terminal when the
|
||||
* original pane is gone. Workspace-scoped chats are reminted onto that
|
||||
* target, duplicate terminal map entries for the same chat are cleared, and
|
||||
* a session panel view is seeded so terminal scopes do not fall back to a
|
||||
* blank draft.
|
||||
*/
|
||||
export function handoffDissolvedWorkspaceAIScope<T extends AISessionScopeHandoffLike>(input: {
|
||||
activeSessionIdMap: AIActiveSessionIdMap;
|
||||
@@ -99,7 +101,7 @@ export function handoffDissolvedWorkspaceAIScope<T extends AISessionScopeHandoff
|
||||
panelViewByScope: Record<string, AIPanelViewHandoffLike>;
|
||||
changed: boolean;
|
||||
} {
|
||||
const preferredTerminalId = (
|
||||
const fallbackTerminalId = (
|
||||
input.preferredTerminalId
|
||||
&& input.terminalIds.includes(input.preferredTerminalId)
|
||||
? input.preferredTerminalId
|
||||
@@ -109,6 +111,17 @@ export function handoffDissolvedWorkspaceAIScope<T extends AISessionScopeHandoff
|
||||
const workspaceKey = buildAIScopeKey('workspace', input.workspaceId);
|
||||
const workspaceActive = input.activeSessionIdMap[workspaceKey];
|
||||
const hasWorkspaceActive = typeof workspaceActive === 'string' && workspaceActive.length > 0;
|
||||
const activeSession = hasWorkspaceActive
|
||||
? input.sessions.find((session) => session.id === workspaceActive)
|
||||
: undefined;
|
||||
const survivingOwnerTerminalId = (
|
||||
activeSession?.scope.type === 'terminal'
|
||||
&& activeSession.scope.targetId
|
||||
&& input.terminalIds.includes(activeSession.scope.targetId)
|
||||
? activeSession.scope.targetId
|
||||
: null
|
||||
);
|
||||
const handoffTargetTerminalId = survivingOwnerTerminalId ?? fallbackTerminalId;
|
||||
const previousPanelViewByScope = (
|
||||
input.panelViewByScope as Record<string, AIPanelViewHandoffLike> | undefined
|
||||
) ?? {};
|
||||
@@ -118,8 +131,8 @@ export function handoffDissolvedWorkspaceAIScope<T extends AISessionScopeHandoff
|
||||
};
|
||||
let mapChanged = false;
|
||||
|
||||
if (preferredTerminalId && hasWorkspaceActive) {
|
||||
const terminalKey = buildAIScopeKey('terminal', preferredTerminalId);
|
||||
if (handoffTargetTerminalId && hasWorkspaceActive) {
|
||||
const terminalKey = buildAIScopeKey('terminal', handoffTargetTerminalId);
|
||||
let map = nextMap;
|
||||
let changed = false;
|
||||
if (map[terminalKey] !== workspaceActive) {
|
||||
@@ -145,24 +158,23 @@ export function handoffDissolvedWorkspaceAIScope<T extends AISessionScopeHandoff
|
||||
|
||||
let sessionsChanged = false;
|
||||
const nextSessions = input.sessions.map((session) => {
|
||||
if (!preferredTerminalId) return session;
|
||||
if (!handoffTargetTerminalId) return session;
|
||||
|
||||
const isWorkspaceScoped = (
|
||||
session.scope.type === 'workspace'
|
||||
&& session.scope.targetId === input.workspaceId
|
||||
);
|
||||
// Inherited member chats stay terminal-scoped under the original pane.
|
||||
// Prefer the focused survivor even when that original pane also survives
|
||||
// dissolve, otherwise B's history ownership filter excludes A's chat.
|
||||
// Keep an inherited chat on its owner when that pane survives. Remint
|
||||
// only when the handoff target is a different terminal (original pane gone).
|
||||
const isInheritedActiveChatOnOtherTerminal = (
|
||||
hasWorkspaceActive
|
||||
&& session.id === workspaceActive
|
||||
&& session.scope.type === 'terminal'
|
||||
&& Boolean(session.scope.targetId)
|
||||
&& session.scope.targetId !== preferredTerminalId
|
||||
&& session.scope.targetId !== handoffTargetTerminalId
|
||||
);
|
||||
if (!isWorkspaceScoped && !isInheritedActiveChatOnOtherTerminal) return session;
|
||||
if (session.scope.type === 'terminal' && session.scope.targetId === preferredTerminalId) {
|
||||
if (session.scope.type === 'terminal' && session.scope.targetId === handoffTargetTerminalId) {
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -172,7 +184,7 @@ export function handoffDissolvedWorkspaceAIScope<T extends AISessionScopeHandoff
|
||||
scope: {
|
||||
...session.scope,
|
||||
type: 'terminal',
|
||||
targetId: preferredTerminalId,
|
||||
targetId: handoffTargetTerminalId,
|
||||
},
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
@@ -182,8 +194,8 @@ export function handoffDissolvedWorkspaceAIScope<T extends AISessionScopeHandoff
|
||||
const nextPanelViewByScope: Record<string, AIPanelViewHandoffLike> = {
|
||||
...previousPanelViewByScope,
|
||||
};
|
||||
if (preferredTerminalId && hasWorkspaceActive) {
|
||||
const terminalKey = buildAIScopeKey('terminal', preferredTerminalId);
|
||||
if (handoffTargetTerminalId && hasWorkspaceActive) {
|
||||
const terminalKey = buildAIScopeKey('terminal', handoffTargetTerminalId);
|
||||
const workspacePanelView = previousPanelViewByScope[workspaceKey];
|
||||
const terminalPanelView = previousPanelViewByScope[terminalKey];
|
||||
const nextView: AIPanelViewHandoffLike = (
|
||||
|
||||
@@ -71,9 +71,17 @@ const POSIX_PROMPT_PATTERN = /^(?:[^\s@]+@[^\s:]+(?::[^\n\r]*)?|\[[^\s@\]]+@[^\s
|
||||
|
||||
// Host line of a two-line bash PS1 (`\u@\h: \w` then a following `$` or `#`).
|
||||
// Same user@host[:path] class as POSIX_PROMPT_PATTERN's first alternative,
|
||||
// without a prompt marker on this line. A bare `$`/`#` alone is not a prompt.
|
||||
// without a prompt marker on this line. Oh My Zsh `ys` uses a different host
|
||||
// line (`# user @ host ...`), matched separately below. A bare `$`/`#` alone
|
||||
// is not a prompt.
|
||||
const POSIX_PROMPT_HOST_LINE_PATTERN = /^[^\s@]+@[^\s:]+(?::[^\n\r]*)?$/;
|
||||
|
||||
// Oh My Zsh `ys` status line: a decorative `#`, then `user @ host` with
|
||||
// spaces around `@`, then an optional remainder (directory, git, venv, time,
|
||||
// exit code). Example: `# user @ host in ~ [12:34:56]`. Arbitrary text before
|
||||
// a bare `$`/`#` still does not match.
|
||||
const OH_MY_ZSH_YS_HOST_LINE_PATTERN = /^#\s+[^\s@]+\s+@\s+[^\s]+(?:\s+[^\n\r]+)?$/;
|
||||
|
||||
// Alpine's default ash PS1 omits the username: `\h:\w\$ ` -> `localhost:~# `.
|
||||
// Require a hostname and a home/absolute path, excluding prompt delimiters
|
||||
// within the path so partially entered commands do not count as idle prompts.
|
||||
@@ -124,12 +132,15 @@ function extractTrailingIdlePrompt(output) {
|
||||
return lastLine;
|
||||
}
|
||||
|
||||
// Two-line PS1 (`export PS1="\u@\h: \w\n$ "`). Bash emits the newline as
|
||||
// CR and the PTY's ONLCR adds another, so the bytes are `\r\r\n`. The
|
||||
// CR-to-LF normalize above turns that into blank lines between the host
|
||||
// line and `$`/`#`. Keep those blanks in the cache: getFreshIdlePrompt
|
||||
// uses the same normalize, and hasExpectedPromptSuffix compares this
|
||||
// CR-as-LF form against live PTY bytes. A lone `$` must not satisfy endsWith.
|
||||
// Two-line PS1. Classic bash is `export PS1="\u@\h: \w\n$ "`. Oh My Zsh
|
||||
// `ys` paints `# user @ host ...` and then a bare `$` or `#`. Bash emits
|
||||
// the newline as CR and the PTY's ONLCR adds another, so the bytes are
|
||||
// `\r\r\n`. The CR-to-LF normalize above turns that into blank lines
|
||||
// between the host line and `$`/`#`. Keep those blanks in the cache:
|
||||
// getFreshIdlePrompt uses the same normalize, and hasExpectedPromptSuffix
|
||||
// compares this CR-as-LF form against live PTY bytes. A lone `$`/`#` must
|
||||
// not satisfy endsWith; the previous non-empty line has to be a recognized
|
||||
// host line (classic `user@host[:path]` or Oh My Zsh `ys`).
|
||||
if (rightTrimmed === "$" || rightTrimmed === "#") {
|
||||
let hostIndex = lines.length - 1;
|
||||
while (hostIndex >= 0 && lines[hostIndex].replace(/\s+$/, "") === "") {
|
||||
@@ -137,7 +148,13 @@ function extractTrailingIdlePrompt(output) {
|
||||
}
|
||||
const previousLine = hostIndex >= 0 ? lines[hostIndex] : "";
|
||||
const previousRightTrimmed = previousLine.replace(/\s+$/, "");
|
||||
if (POSIX_PROMPT_HOST_LINE_PATTERN.test(previousRightTrimmed)) {
|
||||
// Same optional "(name) " env prefix as isDefaultPosixPromptLine (#3947).
|
||||
// Recognition only; the returned prompt keeps the visible prefix.
|
||||
const previousHostLine = previousRightTrimmed.replace(/^\([^()\n\r]+\)\s+/, "");
|
||||
if (
|
||||
POSIX_PROMPT_HOST_LINE_PATTERN.test(previousHostLine)
|
||||
|| OH_MY_ZSH_YS_HOST_LINE_PATTERN.test(previousHostLine)
|
||||
) {
|
||||
return `${lines.slice(hostIndex).join("\n")}\n${lastLine}`;
|
||||
}
|
||||
}
|
||||
@@ -202,8 +219,9 @@ function trackSessionIdlePrompt(session, chunk) {
|
||||
// Return `session.lastIdlePrompt` only if the PTY's recent rolling tail
|
||||
// still ends with it. The cached prompt is updated only when
|
||||
// extractTrailingIdlePrompt recognizes a known shape (PowerShell, cmd.exe,
|
||||
// `user@host[:path][#$]`, two-line `user@host[:path]` + `$`/`#`,
|
||||
// RHEL `[user@host path][#$]`, or Alpine `host:path[#$]`);
|
||||
// `user@host[:path][#$]`, two-line classic `user@host[:path]` or Oh My Zsh
|
||||
// `ys` (`# user @ host ...`) + `$`/`#`, RHEL `[user@host path][#$]`, or
|
||||
// Alpine `host:path[#$]`);
|
||||
// a remote shell switch into another shell, an oh-my-posh / starship / custom
|
||||
// PS1, or any unrecognized prompt would
|
||||
// otherwise leave a stale value behind, which `resolveEffectiveShellKind`
|
||||
|
||||
@@ -262,6 +262,83 @@ test("trackSessionIdlePrompt and getFreshIdlePrompt accept a two-line user@host
|
||||
assert.equal(getFreshIdlePrompt(ptySession), "user@host: /home/foo\n\n\n$ ");
|
||||
});
|
||||
|
||||
test("extractTrailingIdlePrompt recognizes an Oh My Zsh ys two-line idle prompt", () => {
|
||||
// ys paints `# user @ host ...` then a bare `$` or `#`. Spaces around `@`
|
||||
// and the leading decorative `#` are required; a lone marker is not a prompt.
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("# user @ host in ~ [12:34:56]\n$ "),
|
||||
"# user @ host in ~ [12:34:56]\n$ ",
|
||||
);
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("welcome\r\n# user @ host in ~ [12:34:56]\n# "),
|
||||
"# user @ host in ~ [12:34:56]\n# ",
|
||||
);
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("# user @ host in ~/proj on git:main [12:34:56]\n$ "),
|
||||
"# user @ host in ~/proj on git:main [12:34:56]\n$ ",
|
||||
);
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("welcome\r\n# ys @ ys-mbp in ~/.oh-my-zsh on git:master x [21:47:42] C:127\n$ "),
|
||||
"# ys @ ys-mbp in ~/.oh-my-zsh on git:master x [21:47:42] C:127\n$ ",
|
||||
);
|
||||
// Same CR / CRLF blank lines as the classic two-line PS1.
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("# user @ host in ~ [12:34:56]\r\r\n$ "),
|
||||
"# user @ host in ~ [12:34:56]\n\n\n$ ",
|
||||
);
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("# user @ host in ~ [12:34:56]\r\n# "),
|
||||
"# user @ host in ~ [12:34:56]\n\n# ",
|
||||
);
|
||||
// Optional "(name) " env prefix is stripped only for recognition (#3947).
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("(venv) # user @ host in ~ [12:34:56]\n$ "),
|
||||
"(venv) # user @ host in ~ [12:34:56]\n$ ",
|
||||
);
|
||||
assert.equal(
|
||||
extractTrailingIdlePrompt("(venv) user@host: /home/foo\n$ "),
|
||||
"(venv) user@host: /home/foo\n$ ",
|
||||
);
|
||||
|
||||
assert.equal(extractTrailingIdlePrompt("$ "), "");
|
||||
assert.equal(extractTrailingIdlePrompt("# "), "");
|
||||
assert.equal(extractTrailingIdlePrompt("welcome\r\n$ "), "");
|
||||
assert.equal(extractTrailingIdlePrompt("not a host line\n$ "), "");
|
||||
assert.equal(extractTrailingIdlePrompt("user @ host in ~\n$ "), "");
|
||||
assert.equal(extractTrailingIdlePrompt("# user host in ~ [12:34:56]\n$ "), "");
|
||||
assert.equal(extractTrailingIdlePrompt("# user@host in ~ [12:34:56]\n$ "), "");
|
||||
assert.equal(extractTrailingIdlePrompt("copied output\n# "), "");
|
||||
assert.equal(extractTrailingIdlePrompt("# user @ host in ~ [12:34:56]\n$ ls"), "");
|
||||
assert.equal(extractTrailingIdlePrompt("# user @ host in ~ [12:34:56] $"), "");
|
||||
assert.equal(extractTrailingIdlePrompt("echo $"), "");
|
||||
assert.equal(extractTrailingIdlePrompt("price is 5$"), "");
|
||||
assert.equal(extractTrailingIdlePrompt("(venv) not a host line\n$ "), "");
|
||||
});
|
||||
|
||||
test("trackSessionIdlePrompt and getFreshIdlePrompt accept an Oh My Zsh ys two-line prompt", () => {
|
||||
const session = {};
|
||||
const prompt = "# user @ host in ~ [12:34:56]\n$ ";
|
||||
trackSessionIdlePrompt(session, `welcome\r\n${prompt}`);
|
||||
assert.equal(getFreshIdlePrompt(session), prompt);
|
||||
|
||||
const extra = {};
|
||||
trackSessionIdlePrompt(extra, "# user @ host in ~/proj on git:main [12:34:56]\r\r\n$ ");
|
||||
assert.equal(
|
||||
getFreshIdlePrompt(extra),
|
||||
"# user @ host in ~/proj on git:main [12:34:56]\n\n\n$ ",
|
||||
);
|
||||
|
||||
const root = {};
|
||||
trackSessionIdlePrompt(root, "# user @ host in ~ [12:34:56]\n# ");
|
||||
assert.equal(getFreshIdlePrompt(root), "# user @ host in ~ [12:34:56]\n# ");
|
||||
|
||||
const bare = {};
|
||||
assert.equal(trackSessionIdlePrompt(bare, "$ "), "");
|
||||
assert.equal(getFreshIdlePrompt(bare), "");
|
||||
assert.equal(trackSessionIdlePrompt(bare, "not a host line\n$ "), "");
|
||||
assert.equal(getFreshIdlePrompt(bare), "");
|
||||
});
|
||||
|
||||
test("extractTrailingIdlePrompt still recognizes a classic single-line POSIX prompt", () => {
|
||||
assert.equal(extractTrailingIdlePrompt("welcome\r\nuser@host:~$ "), "user@host:~$ ");
|
||||
});
|
||||
|
||||
@@ -4,6 +4,11 @@ A minimal browser plugin that uses Netcatty's selected configured model and one
|
||||
explicitly selected reference. It provides Send, streamed text, Stop, and usage.
|
||||
It has no tools, terminal commands, direct network access, Node runtime, or storage.
|
||||
|
||||
The page follows light/dark mode and accent updates through the existing
|
||||
`netcattyView.onDidChangeEnvironment()` API and seven public HSL theme tokens.
|
||||
Missing tokens fall back to the standard sample palettes. Its theme subscription
|
||||
is disposed on page hide and restored on page show.
|
||||
|
||||
An expired chat is reopened before the next explicit Send. If the configured
|
||||
model changes or a request fails, use **Refresh model** to reopen the session,
|
||||
then click **Send** yourself. Refresh keeps your draft and never retries model
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
:root { color-scheme: dark; font: 15px/1.5 system-ui,sans-serif; color:#e9edf6; background:#10141c; }
|
||||
:root { color-scheme: light dark; --background:0 0% 100%; --foreground:222 47% 12%; --muted:210 20% 96%; --muted-foreground:215 20% 35%; --border:215 20% 75%; --primary:221 83% 45%; --primary-foreground:0 0% 100%; font: 15px/1.5 system-ui,sans-serif; color:hsl(var(--foreground)); background:hsl(var(--background)); }
|
||||
:root[data-theme="dark"] { --background:221 39% 11%; --foreground:210 20% 98%; --muted:219 27% 21%; --muted-foreground:220 13% 91%; --border:218 11% 65%; --primary:213 97% 87%; --primary-foreground:221 39% 11%; }
|
||||
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --background:221 39% 11%; --foreground:210 20% 98%; --muted:219 27% 21%; --muted-foreground:220 13% 91%; --border:218 11% 65%; --primary:213 97% 87%; --primary-foreground:221 39% 11%; } }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; }
|
||||
main { max-width:760px; padding:36px; margin:auto; }
|
||||
header { margin-bottom:26px; }
|
||||
.badge { color:#8bb9ff; font-size:11px; font-weight:700; letter-spacing:1.6px; }
|
||||
.badge { color:hsl(var(--primary)); font-size:11px; font-weight:700; letter-spacing:1.6px; }
|
||||
h1 { font-size:27px; font-weight:650; letter-spacing:-.6px; margin:8px 0; }
|
||||
p { color:#a9b4c7; margin:4px 0; }
|
||||
.card { border:1px solid #303c4f; border-radius:10px; padding:16px 18px; margin:14px 0; background:#181f2a; }
|
||||
label { display:block; font-size:12px; font-weight:600; color:#aabbd5; margin-bottom:8px; }
|
||||
p { color:hsl(var(--muted-foreground)); margin:4px 0; }
|
||||
.card { border:1px solid hsl(var(--border)); border-radius:10px; padding:16px 18px; margin:14px 0; background:hsl(var(--muted)); }
|
||||
label { display:block; font-size:12px; font-weight:600; color:hsl(var(--muted-foreground)); margin-bottom:8px; }
|
||||
#model { font-weight:600; }
|
||||
select, textarea { width:100%; background:#10161f; color:#e9edf6; border:1px solid #455570; padding:10px 12px; border-radius:7px; font:inherit; }
|
||||
select:focus,textarea:focus,button:focus-visible { outline:2px solid #8bb9ff; outline-offset:2px; }
|
||||
select, textarea { width:100%; background:hsl(var(--background)); color:inherit; border:1px solid hsl(var(--border)); padding:10px 12px; border-radius:7px; font:inherit; }
|
||||
select:focus,textarea:focus,button:focus-visible { outline:2px solid hsl(var(--primary)); outline-offset:2px; }
|
||||
textarea { resize:vertical; min-height:84px; }
|
||||
form { margin:22px 0; }
|
||||
.actions { display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-top:12px; }
|
||||
button { border:1px solid #4e8bdf; border-radius:7px; background:#3274c6; color:white; padding:9px 24px; font:inherit; font-weight:600; cursor:pointer; }
|
||||
button { border:1px solid hsl(var(--border)); border-radius:7px; background:transparent; color:inherit; padding:9px 24px; font:inherit; font-weight:600; cursor:pointer; }
|
||||
#send { border-color:transparent; background:hsl(var(--primary)); color:hsl(var(--primary-foreground)); }
|
||||
button:disabled { opacity:.42; cursor:default; }
|
||||
#stop,#refresh,#choose-reference { background:transparent; border-color:#56657b; }
|
||||
#choose-reference { margin-top:10px; }
|
||||
#status { margin-left:4px; color:#b6c6dc; font-size:12px; }
|
||||
#status { margin-left:4px; color:hsl(var(--muted-foreground)); font-size:12px; }
|
||||
.response { min-height:160px; }
|
||||
pre { white-space:pre-wrap; overflow-wrap:anywhere; font:16px/1.7 system-ui,sans-serif; margin:0 0 12px; }
|
||||
.hint,footer { font-size:12px; color:#9aabc3; margin-top:8px; }
|
||||
.hint,footer { font-size:12px; color:hsl(var(--muted-foreground)); margin-top:8px; }
|
||||
footer { padding-top:8px; }
|
||||
@media(max-width:520px) { main { padding:22px; } h1 { font-size:23px; } }
|
||||
@media (forced-colors: active) { button, select, textarea { border:1px solid ButtonText; } }
|
||||
@@ -2,6 +2,31 @@
|
||||
'use strict';
|
||||
const element = (id) => document.getElementById(id);
|
||||
const invoke = (operation, params = {}) => window.netcattyView.executeCommand(`com.netcatty.assistant.${operation}`, params);
|
||||
// Follow the same seven public semantic tokens as the other browser samples.
|
||||
// Only this page's own DOM is updated; no host settings or DOM access is used.
|
||||
(() => {
|
||||
const names = ['--background', '--foreground', '--muted', '--muted-foreground', '--border', '--primary', '--primary-foreground'];
|
||||
let subscription;
|
||||
const subscribe = () => {
|
||||
if (subscription) return;
|
||||
subscription = window.netcattyView.onDidChangeEnvironment?.((environment) => {
|
||||
const root = document.documentElement;
|
||||
const theme = ['light', 'dark'].includes(environment.theme) ? environment.theme : 'system';
|
||||
root.dataset.theme = theme;
|
||||
root.style.colorScheme = theme === 'system' ? 'light dark' : theme;
|
||||
root.dataset.reducedMotion = String(Boolean(environment.reducedMotion));
|
||||
root.dataset.highContrast = String(Boolean(environment.highContrast));
|
||||
for (const name of names) {
|
||||
const value = environment.themeTokens?.[name];
|
||||
if (typeof value === 'string' && value.length > 0 && value.length <= 96) root.style.setProperty(name, value);
|
||||
else root.style.removeProperty(name);
|
||||
}
|
||||
});
|
||||
};
|
||||
subscribe();
|
||||
window.addEventListener('pagehide', () => { subscription?.dispose(); subscription = undefined; });
|
||||
window.addEventListener('pageshow', subscribe);
|
||||
})();
|
||||
let chat;
|
||||
let turn;
|
||||
let generation = 0;
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { localStorageAdapter } from '../../persistence/localStorageAdapter';
|
||||
import { STORAGE_KEY_AI_ACTIVE_MODEL, STORAGE_KEY_AI_ACTIVE_PROVIDER, STORAGE_KEY_AI_PROVIDERS } from '../../config/storageKeys';
|
||||
import type { ProviderConfig } from '../types';
|
||||
import { netcattyBridge } from '../../services/netcattyBridge';
|
||||
|
||||
// Host renderer only. Read the existing configured provider/model without
|
||||
// loading AgentRuntime or syncing/inheriting the Agent's permission policy.
|
||||
export function getConfiguredPluginModel() {
|
||||
const providers = localStorageAdapter.read<ProviderConfig[]>(STORAGE_KEY_AI_PROVIDERS) ?? [];
|
||||
return selectedModel(providers);
|
||||
}
|
||||
|
||||
function selectedModel(providers: ProviderConfig[]) {
|
||||
const activeProviderId = localStorageAdapter.readString(STORAGE_KEY_AI_ACTIVE_PROVIDER);
|
||||
const activeModelId = localStorageAdapter.readString(STORAGE_KEY_AI_ACTIVE_MODEL);
|
||||
const provider = providers.find((item) => item.id === activeProviderId && item.enabled);
|
||||
@@ -14,3 +19,15 @@ export function getConfiguredPluginModel() {
|
||||
if (!config.defaultModel) return null;
|
||||
return { config, label: `${config.name} · ${config.defaultModel}`.slice(0, 160), revision: JSON.stringify(config) };
|
||||
}
|
||||
|
||||
// The existing main provider transport needs its host-owned registry for both
|
||||
// credentials and endpoint admission. Do not depend on mounting the AI sidebar,
|
||||
// or replace the registry with a plugin-supplied URL / just one selected entry.
|
||||
export async function prepareConfiguredPluginModel(revision: string) {
|
||||
const providers = localStorageAdapter.read<ProviderConfig[]>(STORAGE_KEY_AI_PROVIDERS) ?? [];
|
||||
if (selectedModel(providers)?.revision !== revision) throw Error('Selected model changed');
|
||||
const bridge = netcattyBridge.require();
|
||||
if (!bridge.aiSyncProviders || !(await bridge.aiSyncProviders(providers)).ok) {
|
||||
throw Error('Host provider configuration is unavailable');
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const syntheticModel = { config: { id: 'synthetic', name: 'Synthetic', providerI
|
||||
|
||||
test('renderer-owned model handles never serialize provider config and cannot cross runtimes', async () => {
|
||||
let active = true;
|
||||
const runtime = createTrustedPluginAIHostRuntime({ getModel: () => syntheticModel, chooseSelected: async () => [], isActive: () => active });
|
||||
const runtime = createTrustedPluginAIHostRuntime({ getModel: () => syntheticModel, prepareModel: async () => {}, chooseSelected: async () => [], isActive: () => active });
|
||||
const model = await runtime.handle('model.select', {}, 'runtime-a') as { hostHandle: string; label: string };
|
||||
assert.doesNotMatch(JSON.stringify(model), /synthetic-not-a-secret|apiKey|providerId|config/);
|
||||
await assert.rejects(runtime.handle('model.current', model, 'runtime-b'));
|
||||
@@ -36,7 +36,7 @@ test('selected metadata uses explicit safe projections and current resource iden
|
||||
|
||||
test('selected summaries never read content; late resource changes and runtime release deny content', async () => {
|
||||
let current = true; let reads = 0; let resolve: (text: string) => void;
|
||||
const runtime = createTrustedPluginAIHostRuntime({ getModel: () => syntheticModel, isActive: () => true,
|
||||
const runtime = createTrustedPluginAIHostRuntime({ getModel: () => syntheticModel, prepareModel: async () => {}, isActive: () => true,
|
||||
chooseSelected: async () => [{ key: 'note', scope: 'note', title: 'Synthetic', version: '1', isCurrent: () => current,
|
||||
readText: () => { reads++; return new Promise<string>((r) => { resolve = r; }); } }] });
|
||||
const result = await runtime.handle('context.select', {}, 'runtime-a') as { items: Array<{ hostHandle: string; version: string }> };
|
||||
@@ -46,3 +46,22 @@ test('selected summaries never read content; late resource changes and runtime r
|
||||
await new Promise(setImmediate); current = false; resolve!('Late synthetic text'); await assert.rejects(reading);
|
||||
runtime.releaseRuntime('runtime-a'); await assert.rejects(runtime.handle('context.current', reference, 'runtime-a')); runtime.shutdown();
|
||||
});
|
||||
|
||||
test('provider preparation failures and late results cannot publish a model handle after release, cancellation or model change', async () => {
|
||||
for (const action of ['failure', 'release', 'cancel', 'change', 'shutdown']) {
|
||||
let resume!: () => void; let current = syntheticModel;
|
||||
const waiting = new Promise<void>((resolve) => { resume = resolve; });
|
||||
const controller = new AbortController();
|
||||
const runtime = createTrustedPluginAIHostRuntime({ getModel: () => current, isActive: () => true, chooseSelected: async () => [],
|
||||
prepareModel: async () => { await waiting; if (action === 'failure') throw Error('Synthetic sync denied'); } });
|
||||
const selecting = runtime.handle('model.select', {}, 'runtime-a', controller.signal);
|
||||
const rejection = assert.rejects(selecting);
|
||||
if (action === 'release') runtime.releaseRuntime('runtime-a');
|
||||
if (action === 'cancel') controller.abort();
|
||||
if (action === 'change') current = { ...syntheticModel, revision: '2' };
|
||||
if (action === 'shutdown') runtime.shutdown();
|
||||
resume(); await rejection;
|
||||
await assert.rejects(runtime.handle('model.current', { hostHandle: 'not-published' }, 'runtime-a'));
|
||||
runtime.shutdown();
|
||||
}
|
||||
});
|
||||
@@ -17,10 +17,12 @@ type Owned<T> = T & { runtimeId: string };
|
||||
|
||||
export function createTrustedPluginAIHostRuntime(options: {
|
||||
getModel: () => { config: ProviderConfig; label: string; revision: string } | null;
|
||||
prepareModel: (revision: string) => Promise<void>;
|
||||
chooseSelected: (signal: AbortSignal, runtimeId: string) => Promise<HostSelectedSource[]>;
|
||||
isActive: () => boolean;
|
||||
}) {
|
||||
const models = new Map<string, Owned<{ model: Model }>>();
|
||||
const selections = new Set<{ runtimeId: string; active: boolean }>();
|
||||
const turns = new Map<string, Owned<{ iterator: Iterator; controller: AbortController; reading: boolean }>>();
|
||||
const sources = new Map<string, Owned<{ source: HostSelectedSource }>>();
|
||||
let closed = false;
|
||||
@@ -37,6 +39,7 @@ export function createTrustedPluginAIHostRuntime(options: {
|
||||
void turn.iterator.return(undefined).catch(() => {});
|
||||
};
|
||||
const releaseRuntime = (runtimeId: string) => {
|
||||
for (const selection of selections) if (selection.runtimeId === runtimeId) { selection.active = false; selections.delete(selection); }
|
||||
for (const [handle, turn] of turns) if (turn.runtimeId === runtimeId) cancel(handle);
|
||||
for (const [handle, model] of models) if (model.runtimeId === runtimeId) models.delete(handle);
|
||||
for (const [handle, source] of sources) if (source.runtimeId === runtimeId) sources.delete(handle);
|
||||
@@ -50,12 +53,19 @@ export function createTrustedPluginAIHostRuntime(options: {
|
||||
if (kind === 'model.select') {
|
||||
const selected = options.getModel();
|
||||
if (!selected || !selected.config.enabled || !selected.config.defaultModel) throw Error('Choose a configured model first');
|
||||
if (models.size >= 32) throw Error('Model handle limit reached');
|
||||
const handle = crypto.randomUUID();
|
||||
const model = createPluginModelAdapter({ config: selected.config, label: selected.label, transportTag: runtimeId,
|
||||
isCurrent: () => !closed && options.isActive() && options.getModel()?.revision === selected.revision });
|
||||
models.set(handle, { runtimeId, model });
|
||||
return { hostHandle: handle, label: selected.label };
|
||||
if (models.size + selections.size >= 32) throw Error('Model handle limit reached');
|
||||
const selection = { runtimeId, active: true };
|
||||
selections.add(selection);
|
||||
try {
|
||||
await options.prepareModel(selected.revision);
|
||||
signal.throwIfAborted(); assertActive();
|
||||
if (!selection.active || options.getModel()?.revision !== selected.revision) throw Error('Selected model changed');
|
||||
const handle = crypto.randomUUID();
|
||||
const model = createPluginModelAdapter({ config: selected.config, label: selected.label, transportTag: runtimeId,
|
||||
isCurrent: () => !closed && options.isActive() && options.getModel()?.revision === selected.revision });
|
||||
models.set(handle, { runtimeId, model });
|
||||
return { hostHandle: handle, label: selected.label };
|
||||
} finally { selections.delete(selection); }
|
||||
}
|
||||
if (kind === 'model.release') { owned(models, payload.hostHandle, runtimeId); models.delete(String(payload.hostHandle)); return { ok: true }; }
|
||||
if (kind === 'model.current') return { current: await owned(models, payload.hostHandle, runtimeId).model.isCurrent() };
|
||||
@@ -138,6 +148,6 @@ export function createTrustedPluginAIHostRuntime(options: {
|
||||
throw Error('Unsupported host operation');
|
||||
},
|
||||
releaseRuntime,
|
||||
shutdown() { closed = true; for (const handle of [...turns.keys()]) cancel(handle); models.clear(); sources.clear(); },
|
||||
shutdown() { closed = true; for (const selection of selections) selection.active = false; selections.clear(); for (const handle of [...turns.keys()]) cancel(handle); models.clear(); sources.clear(); },
|
||||
};
|
||||
}
|
||||
@@ -9,12 +9,24 @@ async function fixture(t, options = {}) {
|
||||
const root = path.resolve(__dirname, "../examples/plugins/assistant");
|
||||
const dom = new JSDOM(fs.readFileSync(path.join(root, "panel.html"), "utf8"), { runScripts: "outside-only" });
|
||||
t.after(() => dom.window.close());
|
||||
const stylesheet = dom.window.document.createElement("style");
|
||||
stylesheet.textContent = fs.readFileSync(path.join(root, "panel.css"), "utf8");
|
||||
dom.window.document.head.append(stylesheet);
|
||||
let now = 0; let opens = 0; let modelCalls = 0; let changed = false;
|
||||
const calls = [];
|
||||
const environmentListeners = new Set(); let environment;
|
||||
const element = (id) => dom.window.document.getElementById(id);
|
||||
dom.window.Date.now = options.clock ?? (() => now);
|
||||
Object.defineProperty(dom.window, "netcattyView", { value: {
|
||||
onDidReceiveMessage: () => () => {},
|
||||
onDidChangeEnvironment: (listener) => {
|
||||
environmentListeners.add(listener);
|
||||
if (environment) {
|
||||
const replay = environment;
|
||||
queueMicrotask(() => { if (environmentListeners.has(listener) && replay === environment) listener(replay); });
|
||||
}
|
||||
return { dispose: () => environmentListeners.delete(listener) };
|
||||
},
|
||||
executeCommand: async (id, params) => {
|
||||
const operation = id.replace("com.netcatty.assistant.", ""); calls.push({ operation, params });
|
||||
if (options.commands) return options.commands(operation, structuredClone(params));
|
||||
@@ -45,11 +57,43 @@ async function fixture(t, options = {}) {
|
||||
dom.window.eval(fs.readFileSync(path.join(root, "panel.js"), "utf8"));
|
||||
await waitFor(() => element("status").textContent === "Ready");
|
||||
element("prompt").value = "Synthetic draft must survive session recovery";
|
||||
return { element, calls, waitFor, opens: () => opens, modelCalls: () => modelCalls,
|
||||
return { element, calls, waitFor, window: dom.window, opens: () => opens, modelCalls: () => modelCalls,
|
||||
emitEnvironment: (value) => { environment = value; for (const listener of environmentListeners) listener(value); },
|
||||
environmentListeners: () => environmentListeners.size,
|
||||
timeTo: (value) => { now = value; }, changeModel: () => { changed = true; },
|
||||
send: () => element("compose").dispatchEvent(new dom.window.Event("submit", { cancelable: true })) };
|
||||
}
|
||||
|
||||
test("assistant follows host light/dark and accent updates with safe fallbacks and subscription cleanup", async (t) => {
|
||||
const f = await fixture(t); const root = f.window.document.documentElement;
|
||||
const computed = () => f.window.getComputedStyle(root);
|
||||
const calls = f.calls.length;
|
||||
assert.equal(computed().colorScheme, "light dark");
|
||||
assert.equal(computed().getPropertyValue("--background").trim(), "0 0% 100%");
|
||||
assert.equal(f.environmentListeners(), 1);
|
||||
f.emitEnvironment({ theme: "light", themeTokens: { "--background": "20 20% 98%", "--primary": "145 60% 35%", "--muted": "30 10% 90%", "--private-token": "ignored" } });
|
||||
assert.equal(root.style.colorScheme, "light"); assert.equal(root.dataset.theme, "light");
|
||||
assert.equal(root.style.getPropertyValue("--primary"), "145 60% 35%");
|
||||
assert.equal(root.style.getPropertyValue("--private-token"), "");
|
||||
f.emitEnvironment({ theme: "dark", themeTokens: { "--background": "220 20% 10%", "--primary": "280 80% 75%" } });
|
||||
assert.equal(root.style.colorScheme, "dark");
|
||||
assert.equal(root.style.getPropertyValue("--primary"), "280 80% 75%");
|
||||
assert.equal(root.style.getPropertyValue("--muted"), "");
|
||||
assert.equal(computed().getPropertyValue("--muted").trim(), "219 27% 21%");
|
||||
f.emitEnvironment({ theme: "light", themeTokens: { "--primary": "x".repeat(97) } });
|
||||
assert.equal(root.style.colorScheme, "light"); assert.equal(root.style.getPropertyValue("--primary"), "");
|
||||
assert.equal(computed().getPropertyValue("--background").trim(), "0 0% 100%");
|
||||
f.emitEnvironment({ theme: "system" }); assert.equal(root.style.colorScheme, "light dark");
|
||||
assert.equal(f.calls.length, calls); assert.equal(f.modelCalls(), 0);
|
||||
f.window.dispatchEvent(new f.window.Event("pagehide")); assert.equal(f.environmentListeners(), 0);
|
||||
f.emitEnvironment({ theme: "dark", themeTokens: { "--primary": "210 90% 70%" } });
|
||||
assert.equal(root.style.colorScheme, "light dark");
|
||||
f.window.dispatchEvent(new f.window.Event("pageshow"));
|
||||
await new Promise(setImmediate);
|
||||
assert.equal(f.environmentListeners(), 1); assert.equal(root.style.colorScheme, "dark");
|
||||
assert.equal(root.style.getPropertyValue("--primary"), "210 90% 70%");
|
||||
});
|
||||
|
||||
test("expired sample chat reopens before one explicit send and preserves the draft", async (t) => {
|
||||
const f = await fixture(t); f.timeTo(300_001); f.send();
|
||||
await f.waitFor(() => !f.element("prompt").disabled);
|
||||
|
||||
Reference in new issue
Block a user