dsh-side-chat-plus 0.4.0-alpha.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +329 -335
- package/README.zh.md +272 -275
- package/docs/official-sidebar-assessment.md +32 -0
- package/dsh.plugin.json +2 -2
- package/lib/client-registry.js +143 -143
- package/lib/client-registry.js.map +1 -1
- package/lib/client.js +143 -143
- package/lib/client.js.map +1 -1
- package/lib/index.js +110 -28
- package/lib/types/context-types.d.ts +15 -6
- package/lib/types/index.d.ts +3 -0
- package/lib/types/settings-shared.d.ts +1 -3
- package/package.json +45 -42
- package/src/client/attachments/AttachmentRail.module.css +89 -89
- package/src/client/attachments/AttachmentRail.tsx +173 -173
- package/src/client/attachments/DropOverlay.module.css +38 -38
- package/src/client/attachments/DropOverlay.tsx +62 -62
- package/src/client/attachments/ImageLightbox.module.css +44 -44
- package/src/client/attachments/ImageLightbox.tsx +58 -58
- package/src/client/attachments/MessageImage.module.css +61 -61
- package/src/client/attachments/MessageImage.tsx +120 -120
- package/src/client/attachments/index.ts +19 -19
- package/src/client/client.module.css +1051 -1051
- package/src/client/index.tsx +2280 -2283
- package/src/client/locales.ts +201 -201
- package/src/context-types.ts +464 -454
- package/src/index.ts +130 -30
- package/src/settings-shared.ts +1 -4
package/src/index.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-side-chat host half: the /sidechat JSON API. Every side chat is an
|
|
3
3
|
* ORDINARY session (no `origin: 'subagent'`) whose `meta.parentSession` points
|
|
4
|
-
* at the conversation that launched it, archived
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* at the conversation that launched it, kept archived so it appears in neither
|
|
5
|
+
* the main session list nor the subagent catalog, and driven directly through
|
|
6
|
+
* the live agent (followup). Model / reasoning-effort / permission are
|
|
7
7
|
* inherited from the launching conversation at creation and adjustable later.
|
|
8
8
|
*
|
|
9
|
+
* DSH 0.1.7 admits archived sessions to no model step: its archived-session
|
|
10
|
+
* gate answers `{ kind: 'reject' }` on `agent/pre-step` for a session in the
|
|
11
|
+
* registry archive set, so a turn ends `blocked` before the first request. The
|
|
12
|
+
* archive is therefore lifted (unarchived) for exactly as long as a side chat
|
|
13
|
+
* has work queued and restored as soon as its agent settles — see
|
|
14
|
+
* `deliver`/`settleTurn` below.
|
|
15
|
+
*
|
|
9
16
|
* All routes pass the same browser-trust fence as the /api gateway (loopback
|
|
10
17
|
* or trusted authority; cross-site markers refuse).
|
|
11
18
|
*/
|
|
@@ -14,7 +21,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
|
14
21
|
import { dirname } from 'node:path'
|
|
15
22
|
import * as agentApi from '@deepseek-ai/dsh-agent'
|
|
16
23
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
|
17
|
-
import { SettingsConflictError
|
|
24
|
+
import { SettingsConflictError } from '@deepseek-ai/dsh-settings'
|
|
18
25
|
import z from 'schemastery'
|
|
19
26
|
import type {
|
|
20
27
|
Context,
|
|
@@ -27,7 +34,7 @@ import type {
|
|
|
27
34
|
SideSession,
|
|
28
35
|
SideSessionEvent,
|
|
29
36
|
} from './context-types.ts'
|
|
30
|
-
import { SUBCHAT_PREFS_DEFAULTS,
|
|
37
|
+
import { SUBCHAT_PREFS_DEFAULTS, type SubchatPrefs } from './settings-shared.ts'
|
|
31
38
|
import { isTrustedApiRequest } from './trust-fence.ts'
|
|
32
39
|
import { optionalBoolean, readJsonBody, requireString, SidechatError, writeError, writeJson, writeOk } from './wire.ts'
|
|
33
40
|
|
|
@@ -64,6 +71,12 @@ interface SidechatRecord {
|
|
|
64
71
|
/** Live selection coupled to the agent; mutated by `sidechat.selectModel`. */
|
|
65
72
|
selection: SideModelSelectionRef
|
|
66
73
|
createdAt: number
|
|
74
|
+
/**
|
|
75
|
+
* Deliveries whose turn has not settled yet. The session stays unarchived
|
|
76
|
+
* while this is non-zero and is archived again when it reaches zero, so a
|
|
77
|
+
* followup sent mid-turn can never be archived out from under itself.
|
|
78
|
+
*/
|
|
79
|
+
pendingTurns: number
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
/** Durable record list file (cleanup is a later feature; the list is the record). */
|
|
@@ -190,6 +203,10 @@ const PrefsSchema: z<SubchatPrefs> = z.object({
|
|
|
190
203
|
panelHome: z.union(['floating', 'sidebar-right']).default(SUBCHAT_PREFS_DEFAULTS.panelHome),
|
|
191
204
|
})
|
|
192
205
|
|
|
206
|
+
// DSH 0.1.7 derives settings from the Loader entry's live Config fields.
|
|
207
|
+
// Volatile preferences update without disposing the running side chats.
|
|
208
|
+
export const Config = PrefsSchema.volatile()
|
|
209
|
+
|
|
193
210
|
/** Live settings face (bound when the settings service is mounted). */
|
|
194
211
|
interface SubchatSettingsFace {
|
|
195
212
|
get(): { value?: unknown; revision?: number }
|
|
@@ -361,7 +378,68 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
|
|
|
361
378
|
}
|
|
362
379
|
}
|
|
363
380
|
|
|
364
|
-
/**
|
|
381
|
+
/**
|
|
382
|
+
* Serialize one side chat's archive/unarchive writes. Both registry calls go
|
|
383
|
+
* through the host registry's own operation queue, so enqueueing them in call
|
|
384
|
+
* order is what decides the final state; this chain keeps that order stable
|
|
385
|
+
* when a settling turn and a new delivery overlap.
|
|
386
|
+
*/
|
|
387
|
+
const visibilityQueue = new Map<string, Promise<void>>()
|
|
388
|
+
const queueVisibility = (childId: string, op: () => Promise<void>): Promise<void> => {
|
|
389
|
+
const previous = visibilityQueue.get(childId) ?? Promise.resolve()
|
|
390
|
+
const next = previous.then(op, op)
|
|
391
|
+
visibilityQueue.set(childId, next.then(() => {}, () => {}))
|
|
392
|
+
return next
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Restore one side chat's session before a turn. Registries older than the
|
|
397
|
+
* 0.1.7 gate have no `unarchiveSession` (and need none): there the archive
|
|
398
|
+
* simply stays in place for the whole turn. A failed restore propagates: the
|
|
399
|
+
* caller must not deliver a prompt the gate will answer with `blocked`.
|
|
400
|
+
*/
|
|
401
|
+
const unarchiveChild = (childId: string): Promise<void> => {
|
|
402
|
+
const unarchive = ctx.workspaceRegistry.unarchiveSession
|
|
403
|
+
if (unarchive === undefined) return archiveChild(childId)
|
|
404
|
+
return queueVisibility(childId, () => unarchive.call(ctx.workspaceRegistry, childId))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Hide one side chat's session again once its agent has settled. Best-effort:
|
|
409
|
+
* a registry that reports the session active (or refuses the write) only
|
|
410
|
+
* leaves it visible until the next settle, never breaks a turn.
|
|
411
|
+
*/
|
|
412
|
+
const archiveChild = (childId: string): Promise<void> => {
|
|
413
|
+
return queueVisibility(childId, () => ctx.workspaceRegistry.archiveSession(childId)).catch(() => {})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Count one settled turn and re-archive the session when the last one ends. */
|
|
417
|
+
const settleTurn = (record: SidechatRecord): void => {
|
|
418
|
+
if (record.pendingTurns > 0) record.pendingTurns -= 1
|
|
419
|
+
if (record.pendingTurns > 0) return
|
|
420
|
+
void archiveChild(record.childId)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Hand one prompt to a side chat: lift the archive, deliver, and let the
|
|
425
|
+
* agent's own idle boundary put the session back out of every list. `followup`
|
|
426
|
+
* opens the driver synchronously (the phase turns `running` before it
|
|
427
|
+
* returns), so observing `whenIdle` right after it can never miss the turn.
|
|
428
|
+
*/
|
|
429
|
+
const deliver = async (record: SidechatRecord, message: unknown): Promise<void> => {
|
|
430
|
+
record.pendingTurns += 1
|
|
431
|
+
try {
|
|
432
|
+
await unarchiveChild(record.childId)
|
|
433
|
+
record.handle.agent.followup(message)
|
|
434
|
+
} catch (error) {
|
|
435
|
+
// Balance the count: the delivery never opened a turn.
|
|
436
|
+
settleTurn(record)
|
|
437
|
+
throw error
|
|
438
|
+
}
|
|
439
|
+
void record.handle.agent.whenIdle().then(() => { settleTurn(record) }, () => { settleTurn(record) })
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Create one side chat: inherit, record, deliver the first prompt (hidden while idle). */
|
|
365
443
|
const start = async (payload: unknown): Promise<{ childId: string; provider: string; model: string; reasoningEffort?: string }> => {
|
|
366
444
|
const parentSessionId = requireString(payload, 'parentSessionId')
|
|
367
445
|
const content = requireContent(payload)
|
|
@@ -450,19 +528,18 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
|
|
|
450
528
|
console.warn('[dsh-side-chat] permission inherit failed:', error instanceof Error ? error.message : String(error))
|
|
451
529
|
}
|
|
452
530
|
|
|
453
|
-
|
|
454
|
-
await ctx.workspaceRegistry.archiveSession(childId)
|
|
455
|
-
|
|
456
|
-
sideChats.set(childId, {
|
|
531
|
+
const child: SidechatRecord = {
|
|
457
532
|
childId,
|
|
458
533
|
parentSessionId,
|
|
459
534
|
handle,
|
|
460
535
|
selection,
|
|
461
536
|
createdAt: Date.now(),
|
|
462
|
-
|
|
537
|
+
pendingTurns: 0,
|
|
538
|
+
}
|
|
539
|
+
sideChats.set(childId, child)
|
|
463
540
|
persist()
|
|
464
541
|
|
|
465
|
-
|
|
542
|
+
await deliver(child, userMessage(await durableContent(content, lookupEnabled)))
|
|
466
543
|
return { childId, provider, model, ...(reasoningEffort === undefined ? {} : { reasoningEffort }) }
|
|
467
544
|
}
|
|
468
545
|
|
|
@@ -472,7 +549,17 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
|
|
|
472
549
|
const content = requireContent(payload)
|
|
473
550
|
const lookupEnabled = optionalBoolean(payload, 'lookupEnabled')
|
|
474
551
|
const child = childOf(childId)
|
|
475
|
-
|
|
552
|
+
const record = sideChats.get(childId)
|
|
553
|
+
const message = userMessage(await durableContent(content, lookupEnabled))
|
|
554
|
+
if (record === undefined) {
|
|
555
|
+
// A child adopted from the live agent registry (record lost) still needs
|
|
556
|
+
// its archive lifted before a turn it did not create itself.
|
|
557
|
+
await unarchiveChild(childId)
|
|
558
|
+
child.followup(message)
|
|
559
|
+
void child.whenIdle().then(() => { void archiveChild(childId) }, () => { void archiveChild(childId) })
|
|
560
|
+
return { accepted: true }
|
|
561
|
+
}
|
|
562
|
+
await deliver(record, message)
|
|
476
563
|
return { accepted: true }
|
|
477
564
|
}
|
|
478
565
|
|
|
@@ -651,7 +738,9 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
|
|
|
651
738
|
id: randomUUID(),
|
|
652
739
|
role: 'user',
|
|
653
740
|
content: [{ type: 'text', text }],
|
|
654
|
-
|
|
741
|
+
// V4 rejects the retired { kind: 'plugin', plugin: ... } wrapper.
|
|
742
|
+
// Match the identity assigned to historical messages by V3→V4 migration.
|
|
743
|
+
source: { kind: 'plugin:dsh-side-chat', form: 'notice', summary },
|
|
655
744
|
})
|
|
656
745
|
return { accepted: true }
|
|
657
746
|
}
|
|
@@ -810,15 +899,11 @@ export function apply(ctx: Context): void {
|
|
|
810
899
|
const sideChats = new Map<string, SidechatRecord>()
|
|
811
900
|
let settingsFace: SubchatSettingsFace | undefined
|
|
812
901
|
|
|
813
|
-
//
|
|
814
|
-
// The client reads/writes it through the plugin's own fenced routes, since
|
|
815
|
-
// the DSH settings RPC domain only serves allowlisted namespaces.
|
|
902
|
+
// Settings namespaces are now Loader entry ids, including user-renamed rows.
|
|
816
903
|
ctx.inject(['settings'], (sctx: Context) => {
|
|
817
|
-
const ns =
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
watch(cb: (next: SubchatPrefs, prev: SubchatPrefs) => void): () => void
|
|
821
|
-
}
|
|
904
|
+
const ns = (ctx as Context & { entry?: { id: string } }).entry?.id
|
|
905
|
+
if (ns === undefined) return
|
|
906
|
+
sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber))
|
|
822
907
|
const viewOf = (): { value?: unknown; revision?: number } => {
|
|
823
908
|
const descriptor = sctx.settings.describe({ redactSecrets: true }).find((c) => c.ns === ns)
|
|
824
909
|
return descriptor === undefined
|
|
@@ -832,26 +917,41 @@ export function apply(ctx: Context): void {
|
|
|
832
917
|
return viewOf()
|
|
833
918
|
},
|
|
834
919
|
}
|
|
835
|
-
|
|
920
|
+
sctx.effect(() => () => { settingsFace = undefined })
|
|
836
921
|
})
|
|
837
922
|
|
|
838
923
|
const api = buildApi(ctx, sideChats, () => settingsFace)
|
|
839
924
|
|
|
840
|
-
// Tear every live side chat down with the plugin fiber
|
|
925
|
+
// Tear every live side chat down with the plugin fiber, and put back the
|
|
926
|
+
// archive of any chat that was mid-turn (a live turn is lifted from the
|
|
927
|
+
// archive for its duration, so an unload would otherwise leave it visible).
|
|
841
928
|
ctx.effect(() => {
|
|
842
929
|
return () => {
|
|
843
|
-
|
|
844
|
-
void record.handle.dispose().catch(() => {})
|
|
845
|
-
}
|
|
930
|
+
const records = [...sideChats.values()]
|
|
846
931
|
sideChats.clear()
|
|
932
|
+
void (async () => {
|
|
933
|
+
for (const record of records) {
|
|
934
|
+
try {
|
|
935
|
+
await record.handle.dispose()
|
|
936
|
+
} catch {
|
|
937
|
+
// A half-disposed agent must not stop the archive restore below.
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
await ctx.workspaceRegistry.archiveSession(record.childId)
|
|
941
|
+
} catch {
|
|
942
|
+
// Best-effort: a still-active session stays visible until next run.
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
})()
|
|
847
946
|
}
|
|
848
947
|
}, 'dsh-side-chat: dispose side chats')
|
|
849
948
|
|
|
850
949
|
// DSH 0.1.6 lists archived sessions in Settings and lets the user restore
|
|
851
950
|
// one. A side chat is archived precisely so it stays out of the session
|
|
852
|
-
// list, so at activation re-archive every recorded side chat that a restore
|
|
853
|
-
//
|
|
854
|
-
//
|
|
951
|
+
// list, so at activation re-archive every recorded side chat that a restore,
|
|
952
|
+
// an interrupted run (turn still open when the harness stopped), or an older
|
|
953
|
+
// run left visible again. Best-effort: records whose session no longer
|
|
954
|
+
// exists are simply skipped.
|
|
855
955
|
ctx.effect(() => {
|
|
856
956
|
void (async () => {
|
|
857
957
|
for (const record of await readRecords()) {
|
package/src/settings-shared.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared side-chat preference vocabulary (types + constants), consumed by
|
|
3
|
-
* BOTH halves: the host
|
|
3
|
+
* BOTH halves: the host exports a live Config schema over these values
|
|
4
4
|
* (index.ts) and the client reads/writes them through the plugin's own
|
|
5
5
|
* fenced /sidechat settings routes. Kept free of schemastery so the browser
|
|
6
6
|
* bundle never pulls the schema runtime in.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/** The user-settings namespace holding the side-chat preferences. */
|
|
10
|
-
export const SUBCHAT_PREFS_NS = 'dsh-side-chat'
|
|
11
|
-
|
|
12
9
|
/** How a brought-back reply lands in the main conversation. */
|
|
13
10
|
export type BringMode = 'draft' | 'context'
|
|
14
11
|
|