dsh-ntr 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/cordis.patch.yml +17 -1
- package/lib/bridge.js +607 -18
- package/lib/client/acp-ui.js +103 -3
- package/lib/client/acp-view.js +24 -4
- package/lib/client/branch-action.js +133 -0
- package/lib/client/composer-mode.js +48 -13
- package/lib/client/context-meter.js +86 -0
- package/lib/client/index.js +238 -15
- package/lib/client/panel.js +96 -29
- package/lib/client/released-banner.js +66 -0
- package/lib/client/session-controls.js +38 -13
- package/lib/client/slash-isolation.js +2 -2
- package/lib/client/store.js +348 -13
- package/lib/client.js +1138 -67
- package/lib/client.js.map +1 -1
- package/lib/impl.js +54 -1
- package/lib/index.js +7 -0
- package/lib/protocol.js +19 -0
- package/lib/types/bridge.d.ts +229 -6
- package/lib/types/client/acp-ui.d.ts +34 -2
- package/lib/types/client/acp-view.d.ts +13 -2
- package/lib/types/client/branch-action.d.ts +40 -0
- package/lib/types/client/composer-mode.d.ts +28 -0
- package/lib/types/client/context-meter.d.ts +51 -0
- package/lib/types/client/released-banner.d.ts +34 -0
- package/lib/types/client/slash-isolation.d.ts +1 -1
- package/lib/types/client/store.d.ts +98 -1
- package/lib/types/impl.d.ts +41 -1
- package/lib/types/protocol.d.ts +123 -0
- package/package.json +2 -1
- package/src/bridge.ts +638 -20
- package/src/client/acp-ui.tsx +123 -2
- package/src/client/acp-view.tsx +30 -5
- package/src/client/branch-action.tsx +160 -0
- package/src/client/composer-mode.tsx +59 -13
- package/src/client/context-meter.tsx +128 -0
- package/src/client/index.tsx +265 -14
- package/src/client/panel.tsx +157 -40
- package/src/client/primitives.d.ts +27 -0
- package/src/client/released-banner.tsx +97 -0
- package/src/client/session-controls.tsx +34 -11
- package/src/client/slash-isolation.ts +2 -1
- package/src/client/store.ts +375 -14
- package/src/impl.ts +84 -2
- package/src/index.ts +8 -0
- package/src/protocol.ts +121 -0
package/src/client/index.tsx
CHANGED
|
@@ -13,13 +13,20 @@ import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
|
13
13
|
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
14
14
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
15
15
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
16
|
+
// The chat package's client types carry the SlotMap augmentation for
|
|
17
|
+
// `conversation.chat.assistant-actions` (the branch entry's seat). Type-only:
|
|
18
|
+
// erased at build, and the seat is owned by the host's chat module at runtime.
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-chat/client'
|
|
16
20
|
import { AcpStore, type RpcCall } from './store.ts'
|
|
17
21
|
import { AcpSettingsSection } from './panel.tsx'
|
|
18
22
|
import { AcpComposerMode } from './composer-mode.tsx'
|
|
23
|
+
import { AcpContextMeter } from './context-meter.tsx'
|
|
19
24
|
import { AcpComposerAgentTag, AcpSessionControls, AcpSessionMenu } from './session-controls.tsx'
|
|
20
25
|
import { AcpConversationView } from './acp-view.tsx'
|
|
21
26
|
import { AcpSettingsDialog } from './settings-dialog.tsx'
|
|
22
27
|
import { PermissionDock } from './permission-dock.tsx'
|
|
28
|
+
import { ReleasedBanner } from './released-banner.tsx'
|
|
29
|
+
import { AcpBranchAction } from './branch-action.tsx'
|
|
23
30
|
import {
|
|
24
31
|
createModelSeatClaim,
|
|
25
32
|
createPermissionSeatClaim,
|
|
@@ -28,9 +35,9 @@ import {
|
|
|
28
35
|
} from './model-seat.tsx'
|
|
29
36
|
import type { ModelSeatClaim, SeatSlots } from './model-seat.tsx'
|
|
30
37
|
import {
|
|
31
|
-
acpModeFromPresetEvent, rowAgentPreset, stagedIsAcpMode, stagedSessionOf,
|
|
38
|
+
acpModeFromPresetEvent, ACP_PRESET_ID, rowAgentPreset, stagedIsAcpMode, stagedSessionOf,
|
|
32
39
|
} from './acp-ui.tsx'
|
|
33
|
-
import type { AcpChatInjected, AcpSessionFace, AcpSessionsSnapshot, SessionListSource } from './acp-ui.tsx'
|
|
40
|
+
import type { AcpChatInjected, AcpHostForkFace, AcpSessionFace, AcpSessionsSnapshot, SessionListSource } from './acp-ui.tsx'
|
|
34
41
|
import { createSlashIsolation, slashAcpVerdict } from './slash-isolation.ts'
|
|
35
42
|
import type { SlashSource } from './slash-isolation.ts'
|
|
36
43
|
|
|
@@ -58,11 +65,78 @@ interface StructuralServices {
|
|
|
58
65
|
binding(sessionId: string): { session?: unknown } | undefined
|
|
59
66
|
/** The client runtime's session list store (see `AcpSessionsSnapshot`). */
|
|
60
67
|
list?: SessionListSource
|
|
68
|
+
/**
|
|
69
|
+
* The runtime's own branching primitive (`forkAt`'s engine): forks the DSH
|
|
70
|
+
* session tree at the latest completed turn and resolves the child id.
|
|
71
|
+
* Read structurally and used under try/catch — an absent `fork` is the
|
|
72
|
+
* branch button's `hostUnavailable` state, never a load failure.
|
|
73
|
+
*/
|
|
74
|
+
fork?: (options: { sessionId: string; increaseTitle?: boolean }) => Promise<unknown>
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The renderer's Session scope adapter (`uiSession.adapter`). Its `current`
|
|
78
|
+
* observable is the host's own selection — the session the composer is
|
|
79
|
+
* actually staged on — while the list can retain more than one row in a
|
|
80
|
+
* multi-view layout. Read structurally and used under try/catch: a host
|
|
81
|
+
* without it falls back to the list's main-view scan.
|
|
82
|
+
*/
|
|
83
|
+
uiSession: {
|
|
84
|
+
adapter?: {
|
|
85
|
+
current?: {
|
|
86
|
+
getSnapshot(): { key?: unknown } | undefined
|
|
87
|
+
subscribe?(listener: () => void): () => void
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The shell workspace service (`ctx.get("uiWorkspace")`): `openSession`
|
|
93
|
+
* focuses a session in the main view — the branch click's final step.
|
|
94
|
+
*/
|
|
95
|
+
uiWorkspace: {
|
|
96
|
+
openSession?(sessionId: string): void
|
|
61
97
|
}
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
const browserPrefersZh = typeof navigator !== 'undefined' && /^zh\b/i.test(navigator.language)
|
|
65
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The NTR scope marker (`html[data-ntr-acp]`): the ONLY hook the message-action
|
|
104
|
+
* hiding CSS keys on, so the host's like / dislike / branch icons disappear in
|
|
105
|
+
* exactly the conversations this gate has confirmed to run the ACP preset —
|
|
106
|
+
* and reappear the moment the evidence stops answering (every `standDown`
|
|
107
|
+
* clears it; non-ACP sessions never see it). Idempotent per call.
|
|
108
|
+
*/
|
|
109
|
+
const setScopeMarker = (active: boolean): void => {
|
|
110
|
+
if (typeof document === 'undefined') return
|
|
111
|
+
document.documentElement.toggleAttribute('data-ntr-acp', active)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* One-shot sanity probe for the aria-label hiding: the host's action buttons
|
|
116
|
+
* carry LOCALIZED labels and no data-* hook, so a copy change (or a third
|
|
117
|
+
* language) silently un-hides them while everything else keeps working. Once
|
|
118
|
+
* the marker is on and a turn tail is actually on screen, check that at least
|
|
119
|
+
* one label the rules key on still exists and leave one console line for
|
|
120
|
+
* whoever debugs "the icons came back" — dev-visible, never user UI.
|
|
121
|
+
*/
|
|
122
|
+
let probedHostLabels = false
|
|
123
|
+
const probeHostActionLabels = (): void => {
|
|
124
|
+
if (probedHostLabels || typeof document === 'undefined') return
|
|
125
|
+
// No turn tail rendered yet: the check would answer from nothing, so leave
|
|
126
|
+
// the probe armed for the next activation instead of burning it once.
|
|
127
|
+
if (document.querySelector('[data-turn-tail]') === null) return
|
|
128
|
+
probedHostLabels = true
|
|
129
|
+
const found = document.querySelector([
|
|
130
|
+
'button:not([data-ntr-branch])[aria-label="在新对话中分支"]',
|
|
131
|
+
'button:not([data-ntr-branch])[aria-label="Branch into a new conversation"]',
|
|
132
|
+
'button[aria-label="好的回答"]',
|
|
133
|
+
'button[aria-label="Good response"]',
|
|
134
|
+
].join(', '))
|
|
135
|
+
if (found === null) {
|
|
136
|
+
console.warn('[dsh-ntr] none of the host message-action labels matched; the like/dislike/branch icons may not be hidden in NTR conversations')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
66
140
|
/**
|
|
67
141
|
* Structural view of the shell's locale service (`@deepseek-ai/dsh-client-locale`).
|
|
68
142
|
*
|
|
@@ -160,11 +234,37 @@ export function apply(ctx: Context): void {
|
|
|
160
234
|
}
|
|
161
235
|
}
|
|
162
236
|
|
|
237
|
+
/**
|
|
238
|
+
* The runtime's `sessions.fork`, resolved per call: `undefined` renders the
|
|
239
|
+
* branch button disabled (`hostUnavailable`) instead of hiding it, so a
|
|
240
|
+
* host that gains the primitive later is picked up on the next render.
|
|
241
|
+
*/
|
|
242
|
+
const hostFork = (): AcpHostForkFace | undefined => {
|
|
243
|
+
try {
|
|
244
|
+
const fork = service<StructuralServices['sessions']>('sessions')?.fork
|
|
245
|
+
return typeof fork === 'function' ? { fork } : undefined
|
|
246
|
+
} catch {
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Focus a session in the workspace (the branch click's last step). */
|
|
252
|
+
const openSession = (sessionId: string): void => {
|
|
253
|
+
try {
|
|
254
|
+
service<StructuralServices['uiWorkspace']>('uiWorkspace')?.openSession?.(sessionId)
|
|
255
|
+
} catch {
|
|
256
|
+
// Opening is presentation; a host without the workspace service leaves
|
|
257
|
+
// the branched session in the sidebar list, reachable by hand.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
163
261
|
/** The injected face every ACP conversation entry receives. */
|
|
164
262
|
const chatFace = (sessionId: unknown): AcpChatInjected & { sessionId: string } => ({
|
|
165
263
|
acp: store,
|
|
166
264
|
sessionId: String(sessionId),
|
|
167
265
|
faceFor,
|
|
266
|
+
hostFork,
|
|
267
|
+
openSession,
|
|
168
268
|
})
|
|
169
269
|
|
|
170
270
|
ctx.effect(() => ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
@@ -207,6 +307,13 @@ export function apply(ctx: Context): void {
|
|
|
207
307
|
// (see the note there). Runtime passes the real shares either way.
|
|
208
308
|
}, AcpComposerMode as unknown as SlotComponent<any>)), 'dsh-ntr: composer agent-mode control')
|
|
209
309
|
|
|
310
|
+
// The agent's OWN context occupancy in the composer's bottom info row
|
|
311
|
+
// (`conversation.composer.dock` — the dock below the input card where the
|
|
312
|
+
// host renders its native ContextMeter) is installed by the ACP-mode gate
|
|
313
|
+
// below, with the same lifetime as the NTR view tab and the seats. It is
|
|
314
|
+
// NOT registered unconditionally: in an ordinary conversation this row must
|
|
315
|
+
// hold the host's native meter and nothing of ours (see `holdAuxEntries`).
|
|
316
|
+
|
|
210
317
|
// Its picker rides the composer's overlay seat — the platform's home for
|
|
211
318
|
// menus, so the popup floats above the card instead of being clipped by it.
|
|
212
319
|
ctx.effect(() => ctx.slots.inject('conversation.input.overlay', () => ctx.slots.register({
|
|
@@ -248,6 +355,34 @@ export function apply(ctx: Context): void {
|
|
|
248
355
|
inject: (sessionId: unknown) => chatFace(sessionId),
|
|
249
356
|
}, PermissionDock as unknown as SlotComponent<any>)), 'dsh-ntr: conversation permission dock')
|
|
250
357
|
|
|
358
|
+
// The released-session banner in the same dock seat: when this
|
|
359
|
+
// conversation's agent process is parked (idle release or crash), one row
|
|
360
|
+
// says so and offers the explicit resume — the gesture form of the
|
|
361
|
+
// "next message resumes it" promise. Gated inside the component on the
|
|
362
|
+
// chat-list rows (released banner renders iff nothing live is left).
|
|
363
|
+
ctx.effect(() => ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
364
|
+
name: 'conversation.input.dock',
|
|
365
|
+
id: 'acp-released-banner',
|
|
366
|
+
order: 20,
|
|
367
|
+
inject: (sessionId: unknown) => chatFace(sessionId),
|
|
368
|
+
}, ReleasedBanner as unknown as SlotComponent<any>)), 'dsh-ntr: released-session banner')
|
|
369
|
+
|
|
370
|
+
// The NTR branch entry riding the host's own assistant-actions row
|
|
371
|
+
// (`conversation.chat.assistant-actions` — list, session scope; the
|
|
372
|
+
// positional inject argument is the chat session id, like every other
|
|
373
|
+
// session slot). Registered for EVERY conversation; the component gates
|
|
374
|
+
// itself on the confirmed ACP verdict (non-ACP renders nothing), and the
|
|
375
|
+
// `data-ntr-branch` attribute it stamps is the CSS pass that keeps the
|
|
376
|
+
// hiding rules from swallowing the plugin's own button (same aria-label as
|
|
377
|
+
// the host's). Order 20 puts it after the (hidden) feedback entry's 10 —
|
|
378
|
+
// visually the spot the host's own branch button occupied.
|
|
379
|
+
ctx.effect(() => ctx.slots.inject('conversation.chat.assistant-actions', () => ctx.slots.register({
|
|
380
|
+
name: 'conversation.chat.assistant-actions',
|
|
381
|
+
id: 'ntr-branch',
|
|
382
|
+
order: 20,
|
|
383
|
+
inject: (sessionId: unknown) => chatFace(sessionId),
|
|
384
|
+
}, AcpBranchAction as unknown as SlotComponent<any>)), 'dsh-ntr: message branch action')
|
|
385
|
+
|
|
251
386
|
/**
|
|
252
387
|
* The client runtime's session list store, read structurally. Used to find
|
|
253
388
|
* the session the composer is staged on and whether it runs the ACP preset —
|
|
@@ -259,6 +394,30 @@ export function apply(ctx: Context): void {
|
|
|
259
394
|
return list
|
|
260
395
|
}
|
|
261
396
|
|
|
397
|
+
/**
|
|
398
|
+
* The renderer's current Session scope observable, when this host exposes it.
|
|
399
|
+
* This is the source the host's own main-view picker prefers; the list's
|
|
400
|
+
* retained rows are only the fallback for hosts without the adapter.
|
|
401
|
+
*/
|
|
402
|
+
const currentScopeSource = (): NonNullable<StructuralServices['uiSession']['adapter']>['current'] => {
|
|
403
|
+
try {
|
|
404
|
+
const current = service<StructuralServices['uiSession']>('uiSession')?.adapter?.current
|
|
405
|
+
return current !== undefined && typeof current.getSnapshot === 'function' ? current : undefined
|
|
406
|
+
} catch {
|
|
407
|
+
return undefined
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** The session the composer is actually staged on, if the runtime can say. */
|
|
412
|
+
const currentScopeSessionId = (): string | undefined => {
|
|
413
|
+
try {
|
|
414
|
+
const key = currentScopeSource()?.getSnapshot?.()?.key
|
|
415
|
+
return typeof key === 'string' && key !== '' ? key : undefined
|
|
416
|
+
} catch {
|
|
417
|
+
return undefined
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
262
421
|
/**
|
|
263
422
|
* Chat session ids the ACP-mode tracker has already asked the host about (the
|
|
264
423
|
* `session/chat-list` fallback), mapped to WHEN the last ask went out. The
|
|
@@ -286,6 +445,8 @@ export function apply(ctx: Context): void {
|
|
|
286
445
|
|
|
287
446
|
ctx.effect(() => {
|
|
288
447
|
let stopWatch: (() => void) | undefined
|
|
448
|
+
/** Subscription to the renderer's current Session binding (see `currentScopeSource`). */
|
|
449
|
+
let stopCurrent: (() => void) | undefined
|
|
289
450
|
/**
|
|
290
451
|
* The claim on the host's model seat, held while the staged conversation
|
|
291
452
|
* runs the ACP preset. That cell is where the host's own base sits (the
|
|
@@ -321,6 +482,52 @@ export function apply(ctx: Context): void {
|
|
|
321
482
|
* available yet.
|
|
322
483
|
*/
|
|
323
484
|
let permissionClaim: ModelSeatClaim | undefined
|
|
485
|
+
/**
|
|
486
|
+
* ACP-only auxiliary entries: the NTR view tab and the composer's context
|
|
487
|
+
* ring. Unlike the two seats these are `list` slots with no visibility
|
|
488
|
+
* predicate of their own, so "self-hide in the component" cannot remove a
|
|
489
|
+
* tab from the header or free the native bottom row. They are installed by
|
|
490
|
+
* this gate for exactly the same lifetime as the claims and disposed on
|
|
491
|
+
* every stand-down — a standard conversation then has no NTR tab at all,
|
|
492
|
+
* and the host's native context meter sits alone in its dock row.
|
|
493
|
+
*/
|
|
494
|
+
let viewEntry: (() => void) | undefined
|
|
495
|
+
let meterEntry: (() => void) | undefined
|
|
496
|
+
const releaseAuxEntries = (): void => {
|
|
497
|
+
try { viewEntry?.() } catch {}
|
|
498
|
+
viewEntry = undefined
|
|
499
|
+
try { meterEntry?.() } catch {}
|
|
500
|
+
meterEntry = undefined
|
|
501
|
+
}
|
|
502
|
+
const holdAuxEntries = (): void => {
|
|
503
|
+
// A refused registration leaves the native surface in place (fail-open);
|
|
504
|
+
// the next evaluation retries when the slot is declared.
|
|
505
|
+
if (viewEntry === undefined) {
|
|
506
|
+
try {
|
|
507
|
+
viewEntry = ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
508
|
+
name: 'conversation.view',
|
|
509
|
+
id: 'acp',
|
|
510
|
+
order: 20,
|
|
511
|
+
label: () => label('NTR', 'NTR'),
|
|
512
|
+
inject: (sessionId: unknown) => chatFace(sessionId),
|
|
513
|
+
}, AcpConversationView as unknown as SlotComponent<any>))
|
|
514
|
+
} catch {
|
|
515
|
+
viewEntry = undefined
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (meterEntry === undefined) {
|
|
519
|
+
try {
|
|
520
|
+
meterEntry = ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
521
|
+
name: 'conversation.composer.dock',
|
|
522
|
+
id: 'acp-context-meter',
|
|
523
|
+
order: 10,
|
|
524
|
+
inject: (sessionId: unknown) => chatFace(sessionId),
|
|
525
|
+
}, AcpContextMeter as unknown as SlotComponent<any>))
|
|
526
|
+
} catch {
|
|
527
|
+
meterEntry = undefined
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
324
531
|
/**
|
|
325
532
|
* Last diagnostic line from {@link syncAcpMode}. Every way this gate can
|
|
326
533
|
* end with the native seat still on screen used to be silent — an inner
|
|
@@ -333,7 +540,8 @@ export function apply(ctx: Context): void {
|
|
|
333
540
|
let seatDiag = 'idle'
|
|
334
541
|
let permissionSeatDiag = 'idle'
|
|
335
542
|
/**
|
|
336
|
-
* Hand
|
|
543
|
+
* Hand every ACP-only surface back: both composer cells, the NTR view tab
|
|
544
|
+
* and the context ring.
|
|
337
545
|
*
|
|
338
546
|
* Every path that is not "this staged conversation is CONFIRMED to run the
|
|
339
547
|
* ACP preset" ends here — including the paths that carry no verdict at all
|
|
@@ -355,6 +563,8 @@ export function apply(ctx: Context): void {
|
|
|
355
563
|
// read identically in the one place either is observable.
|
|
356
564
|
permissionSeatDiag = `${reason} (released)`
|
|
357
565
|
disarmVerdictReask()
|
|
566
|
+
setScopeMarker(false)
|
|
567
|
+
releaseAuxEntries()
|
|
358
568
|
if (claim !== undefined) { claim.release(); claim = undefined }
|
|
359
569
|
if (permissionClaim !== undefined) { permissionClaim.release(); permissionClaim = undefined }
|
|
360
570
|
}
|
|
@@ -388,7 +598,13 @@ export function apply(ctx: Context): void {
|
|
|
388
598
|
standDown('no session list service')
|
|
389
599
|
return
|
|
390
600
|
}
|
|
391
|
-
|
|
601
|
+
// Prefer the renderer's own current selection; the list's retained rows
|
|
602
|
+
// are the fallback for hosts without the scope adapter. A multi-view
|
|
603
|
+
// layout can leave several rows retained, and judging the first one (or
|
|
604
|
+
// the one the previous switch left behind) would hold the ACP chrome
|
|
605
|
+
// while an ordinary conversation is on screen — the reported drift.
|
|
606
|
+
const current = currentScopeSessionId()
|
|
607
|
+
const staged = stagedSessionOf(snapshot, current)
|
|
392
608
|
if (staged === undefined) {
|
|
393
609
|
standDown('no staged session (nothing retains the main view)')
|
|
394
610
|
return
|
|
@@ -444,13 +660,23 @@ export function apply(ctx: Context): void {
|
|
|
444
660
|
// nothing. Both cells follow the same evidence — the staged
|
|
445
661
|
// conversation's preset — through one {@link standDown}, so neither can
|
|
446
662
|
// stay claimed after the evidence for it stops answering.
|
|
447
|
-
if (!stagedIsAcpMode(snapshot, acpModeVerdict)) {
|
|
663
|
+
if (!stagedIsAcpMode(snapshot, acpModeVerdict, current)) {
|
|
664
|
+
// The row's own verdict is authoritative even when it says no: fold it
|
|
665
|
+
// into the store so every per-conversation surface (the branch
|
|
666
|
+
// button's ACP gate included) stops rendering without waiting for a
|
|
667
|
+
// host read. A silent row folds nothing — that case is the ask path's.
|
|
668
|
+
if (rowAgentPreset(staged.row) !== undefined) store.noteAcpMode(staged.sessionId, false)
|
|
448
669
|
standDown(`staged ${staged.sessionId} is not in ACP mode`)
|
|
449
670
|
return
|
|
450
671
|
}
|
|
451
672
|
// The row speaks, so the host verdict is not this gate's only fact
|
|
452
|
-
// source and no re-ask timer is needed.
|
|
453
|
-
|
|
673
|
+
// source and no re-ask timer is needed. The same fold in the affirmative
|
|
674
|
+
// direction: the runtime row IS the preset truth, and the branch
|
|
675
|
+
// button's gate reads the folded map, not the snapshot.
|
|
676
|
+
if (rowAgentPreset(staged.row) !== undefined) {
|
|
677
|
+
store.noteAcpMode(staged.sessionId, rowAgentPreset(staged.row) === ACP_PRESET_ID)
|
|
678
|
+
disarmVerdictReask()
|
|
679
|
+
}
|
|
454
680
|
// Stays re-runnable on purpose: the seat is not declared before the
|
|
455
681
|
// composer bar mounts, a registration can be refused, and the cell can be
|
|
456
682
|
// taken from us later. The claim reports each of those through the
|
|
@@ -458,6 +684,8 @@ export function apply(ctx: Context): void {
|
|
|
458
684
|
// next evaluation (store mutation, list notification, preset frame, or a
|
|
459
685
|
// change to the cell itself) simply tries again.
|
|
460
686
|
seatDiag = `staged ${staged.sessionId} is ACP mode`
|
|
687
|
+
setScopeMarker(true)
|
|
688
|
+
probeHostActionLabels()
|
|
461
689
|
claim ??= createModelSeatClaim({
|
|
462
690
|
slots: ctx.slots as unknown as SeatSlots,
|
|
463
691
|
occupant: () => AcpSessionControls,
|
|
@@ -478,6 +706,11 @@ export function apply(ctx: Context): void {
|
|
|
478
706
|
onDiag: (line) => { permissionSeatDiag = `staged ${staged.sessionId} · ${line}` },
|
|
479
707
|
})
|
|
480
708
|
permissionClaim.sync()
|
|
709
|
+
// The tab and the occupancy ring have the same lifetime as the claims:
|
|
710
|
+
// they exist only while this staged conversation is an ACP one, and a
|
|
711
|
+
// stand-down disposes them (a standard conversation must not see an NTR
|
|
712
|
+
// tab, and the host's native bottom row must stand alone there).
|
|
713
|
+
holdAuxEntries()
|
|
481
714
|
// Confirmed ACP on a silent row: the verdict is all the evidence there
|
|
482
715
|
// is, so it gets a re-ask deadline (see {@link ACP_MODE_VERDICT_TTL_MS}).
|
|
483
716
|
if (rowAgentPreset(staged.row) === undefined) armVerdictReask()
|
|
@@ -531,6 +764,18 @@ export function apply(ctx: Context): void {
|
|
|
531
764
|
const attach = (): void => {
|
|
532
765
|
const list = sessionsList()
|
|
533
766
|
if (list !== undefined && stopWatch === undefined) stopWatch = list.subscribe(runGate)
|
|
767
|
+
// The selection can move without the list changing (a tab or sidebar
|
|
768
|
+
// click), and `uiSession.current` is the host's own answer to "which
|
|
769
|
+
// conversation is on screen". Re-run the gate on that transition so the
|
|
770
|
+
// ACP chrome leaves before the newly staged ordinary conversation paints.
|
|
771
|
+
const current = currentScopeSource()
|
|
772
|
+
if (current?.subscribe !== undefined && stopCurrent === undefined) {
|
|
773
|
+
try {
|
|
774
|
+
stopCurrent = current.subscribe(runGate)
|
|
775
|
+
} catch {
|
|
776
|
+
stopCurrent = undefined
|
|
777
|
+
}
|
|
778
|
+
}
|
|
534
779
|
attachPresetFrames()
|
|
535
780
|
runGate()
|
|
536
781
|
}
|
|
@@ -594,7 +839,12 @@ export function apply(ctx: Context): void {
|
|
|
594
839
|
stopCell()
|
|
595
840
|
stopPreset?.()
|
|
596
841
|
stopWatch?.()
|
|
842
|
+
stopCurrent?.()
|
|
597
843
|
disarmVerdictReask()
|
|
844
|
+
// The scope marker dies with the gate: a reload (HMR) must not leave the
|
|
845
|
+
// hiding CSS pointed at conversations nothing confirms anymore.
|
|
846
|
+
setScopeMarker(false)
|
|
847
|
+
releaseAuxEntries()
|
|
598
848
|
if (claim !== undefined) { claim.release(); claim = undefined }
|
|
599
849
|
if (permissionClaim !== undefined) { permissionClaim.release(); permissionClaim = undefined }
|
|
600
850
|
store.noteSeatDiag('gate disposed')
|
|
@@ -636,12 +886,14 @@ export function apply(ctx: Context): void {
|
|
|
636
886
|
sessionsList()?.getSnapshot(),
|
|
637
887
|
sessionId,
|
|
638
888
|
id => store.getSnapshot().acpMode[id],
|
|
889
|
+
currentScopeSessionId(),
|
|
639
890
|
),
|
|
640
891
|
ensureVerdict: async (sessionId) => {
|
|
641
892
|
const held = (): boolean | undefined => slashAcpVerdict(
|
|
642
893
|
sessionsList()?.getSnapshot(),
|
|
643
894
|
sessionId,
|
|
644
895
|
id => store.getSnapshot().acpMode[id],
|
|
896
|
+
currentScopeSessionId(),
|
|
645
897
|
)
|
|
646
898
|
const known = held()
|
|
647
899
|
if (known !== undefined) return known
|
|
@@ -724,11 +976,10 @@ export function apply(ctx: Context): void {
|
|
|
724
976
|
// diffs). Session-scope list entry; the view ring renders it one tab at a
|
|
725
977
|
// time. Meanwhile the agent's prose and thinking ride the NATIVE chat plane
|
|
726
978
|
// through the LLM adapter — see docs/acp-convergence.md §2.1.
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}, AcpConversationView as unknown as SlotComponent<any>)), 'dsh-ntr: ACP view tab')
|
|
979
|
+
//
|
|
980
|
+
// It is installed by the ACP-mode gate above (`holdAuxEntries`) and NOT at
|
|
981
|
+
// plugin load: the host projects every registered `conversation.view` entry
|
|
982
|
+
// into EVERY session's tab strip, so an unconditional registration showed
|
|
983
|
+
// the NTR tab on ordinary conversations too. The gate's lifetime is the
|
|
984
|
+
// only per-session seam the host offers.
|
|
734
985
|
}
|