@umialpha/dsh-client-ui-workspace-groups 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.i18n.yaml +73 -0
- package/README.md +233 -2
- package/README.zh.md +234 -0
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/lib/client.js +5151 -0
- package/lib/index.js +11 -0
- package/lib/types/client/WorkspacePicker.d.ts +59 -0
- package/lib/types/client/contract/slots.d.ts +473 -0
- package/lib/types/client/index.d.ts +39 -0
- package/lib/types/client/locales.d.ts +246 -0
- package/lib/types/client/navigation.d.ts +157 -0
- package/lib/types/client/pin-order.d.ts +31 -0
- package/lib/types/client/rows/AnimatedRows.d.ts +44 -0
- package/lib/types/client/rows/Rows.d.ts +112 -0
- package/lib/types/client/rows/WorkspaceBrowser.d.ts +8 -0
- package/lib/types/client/session-actions/ArchiveSession.d.ts +23 -0
- package/lib/types/client/session-actions/ForkSession.d.ts +9 -0
- package/lib/types/client/session-actions/PinSession.d.ts +14 -0
- package/lib/types/client/session-actions/RenameSession.d.ts +18 -0
- package/lib/types/client/session-actions/RowActionToast.d.ts +8 -0
- package/lib/types/client/session-actions/derived.d.ts +12 -0
- package/lib/types/client/shortcuts.d.ts +46 -0
- package/lib/types/client/stores.d.ts +56 -0
- package/lib/types/client/tree.d.ts +203 -0
- package/lib/types/index.d.ts +9 -0
- package/package.json +115 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 7c0f613105d4008a
|
|
7
|
+
zh: c36c0cdff7e77576
|
|
8
|
+
/deepseek-ai-dsh-client-ui-workspace:
|
|
9
|
+
en: b286adf3aca51710
|
|
10
|
+
zh: 6122f6e7c8015197
|
|
11
|
+
/deepseek-ai-dsh-client-ui-workspace/summary:
|
|
12
|
+
en: 1a76ace6a18e3ab5
|
|
13
|
+
zh: b274d1e98e133851
|
|
14
|
+
/deepseek-ai-dsh-client-ui-workspace/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/deepseek-ai-dsh-client-ui-workspace/use-this-package:
|
|
18
|
+
en: b164014ee565be16
|
|
19
|
+
zh: 6a741647932b93e6
|
|
20
|
+
/deepseek-ai-dsh-client-ui-workspace/use-this-package/reordering-and-view-options:
|
|
21
|
+
en: 8673afc1d72ad077
|
|
22
|
+
zh: 9fbb83a4029c5d10
|
|
23
|
+
/deepseek-ai-dsh-client-ui-workspace/use-this-package/workspace-hierarchy:
|
|
24
|
+
en: 5e049b90c55eddb3
|
|
25
|
+
zh: e06dc8d0d25c013f
|
|
26
|
+
/deepseek-ai-dsh-client-ui-workspace/use-this-package/search:
|
|
27
|
+
en: 9eab3bfbaf98c987
|
|
28
|
+
zh: 54e7e141dae5c9e8
|
|
29
|
+
/deepseek-ai-dsh-client-ui-workspace/use-this-package/managing-sessions:
|
|
30
|
+
en: cafc87e8e0ac4058
|
|
31
|
+
zh: 4c35bc5bfa24ba6c
|
|
32
|
+
/deepseek-ai-dsh-client-ui-workspace/use-this-package/pending-interactions:
|
|
33
|
+
en: 9fb88260380bf093
|
|
34
|
+
zh: 3bace2b96ecd4a82
|
|
35
|
+
/deepseek-ai-dsh-client-ui-workspace/use-this-package/active-schedule-markers:
|
|
36
|
+
en: 04956ad6ba31cc86
|
|
37
|
+
zh: 0e5fb72cfa932d25
|
|
38
|
+
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation:
|
|
39
|
+
en: 721d5a49fe231191
|
|
40
|
+
zh: 3ef1dd720219e00e
|
|
41
|
+
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/the-directory-flow-hole:
|
|
42
|
+
en: 868904dbfb5713f8
|
|
43
|
+
zh: 225906bfef7398bf
|
|
44
|
+
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/session-row-actions:
|
|
45
|
+
en: a3b80d0e844d2929
|
|
46
|
+
zh: 85fe5dfb2968b491
|
|
47
|
+
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/session-row-actions/packaged-client-plugin:
|
|
48
|
+
en: 36934671b80b11ae
|
|
49
|
+
zh: 8e0aea6162e35e77
|
|
50
|
+
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/session-row-actions/dynamic-client-package:
|
|
51
|
+
en: ad4a333892f2fe92
|
|
52
|
+
zh: 3a670fd401f4d7a7
|
|
53
|
+
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/view-state:
|
|
54
|
+
en: 83031e7505fc228a
|
|
55
|
+
zh: 03e17c9428fe29fd
|
|
56
|
+
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/hover-cards:
|
|
57
|
+
en: 34668e3c2e9c77b7
|
|
58
|
+
zh: c8bb72e29a3f2490
|
|
59
|
+
/deepseek-ai-dsh-client-ui-workspace/further-exploration:
|
|
60
|
+
en: 2abbf27abbd71ba4
|
|
61
|
+
zh: ace33fa2ea042357
|
|
62
|
+
/deepseek-ai-dsh-client-ui-workspace/model-experience:
|
|
63
|
+
en: 3120df665299c634
|
|
64
|
+
zh: 1d9f365d091d0d2a
|
|
65
|
+
/deepseek-ai-dsh-client-ui-workspace/model-experience/kv-cache-effect:
|
|
66
|
+
en: ca75c51c89c2c9b0
|
|
67
|
+
zh: de54f3467ef3b9be
|
|
68
|
+
/deepseek-ai-dsh-client-ui-workspace/known-limitations-and-deferred-work:
|
|
69
|
+
en: 00d7b1ddda7390f5
|
|
70
|
+
zh: 1e5a1055d1be4d1d
|
|
71
|
+
/deepseek-ai-dsh-client-ui-workspace/known-limitations-and-deferred-work/dev-note:
|
|
72
|
+
en: e67f633dc6fe7773
|
|
73
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -1,3 +1,234 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Version-pinned Workspace replacement for the dsh web client with an optional Session groups slot."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @umialpha/dsh-client-ui-workspace-groups
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
This package is a fork of DeepSeek Harness's `@deepseek-ai/dsh-client-ui-workspace` at tag `dsh-v0.2.1-alpha.2` (commit `d743267388641bc76f17c45ce8b4c231aed1d32c`). Version `0.1.0` supports only DSH `0.2.1-alpha.2` and adds the optional `sidebar.workspaces.sessionGroups` slot to Workspace browsing. The copied upstream code remains Copyright (c) 2026 DeepSeek and is distributed under the MIT License in this package.
|
|
11
|
+
|
|
12
|
+
Desktop product events use the optional [product analytics service](../product-analytics/README.md); ordinary Web interactions are excluded.
|
|
13
|
+
|
|
14
|
+
## Summary
|
|
15
|
+
|
|
16
|
+
This package lets users browse grouped or flat Session lists, choose a Workspace for a new Session, and manage Workspaces and Sessions through add, rename, reorder, search, fork, archive, and Workspace deletion; the Session row menu and its hover buttons are slot lists that client plugins extend. Pending interactions appear as warning dots, and subagent-origin Sessions remain hidden. An idle, unarchived Session row with active scheduled tasks shows a clock mark, and its hover card lists those tasks. Canonically distinct folder paths remain separate Workspaces. Adding a Workspace requires a composed directory picker; without one, adding is unavailable.
|
|
17
|
+
|
|
18
|
+
## Table of Contents
|
|
19
|
+
|
|
20
|
+
- [Use this package](#use-this-package)
|
|
21
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
22
|
+
- [Further Exploration](#further-exploration)
|
|
23
|
+
- [Model Experience](#model-experience)
|
|
24
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
25
|
+
- [Dev Note](#dev-note)
|
|
26
|
+
|
|
27
|
+
-----
|
|
28
|
+
|
|
29
|
+
<a id="use-this-package"></a>
|
|
30
|
+
## Use this package
|
|
31
|
+
|
|
32
|
+
Use the sidebar to browse Workspaces and their Sessions, reorder them, and start new ones; use the picker in the Session Intent hero to choose a Workspace for a new session. An open Workspace shows five idle, non-blank Sessions by default. Running Sessions, including parents with running children, remain visible in their ordered positions without using that quota; the selected blank **New Session** is also an extra row until its first prompt. Each **Show more** click reveals up to five more idle Sessions; after the final batch, **Show less** restores the initial rows while keeping running Sessions visible. Closing and reopening the Workspace also restores this folded projection.
|
|
33
|
+
|
|
34
|
+
History rows without a stored title use the localized unnamed label (未命名 / Untitled), rather than a directory name. The current blank row remains New Session; other blank rows remain hidden. Rename drafts use the stored title, or start empty when unnamed; unnamed rows do not offer title copying.
|
|
35
|
+
|
|
36
|
+
With the Session groups Client composed, Pinned and Workspace are sibling sidebar sections. Pinned appears above the Workspace header and remains visible in grouped, tree, flat, and search views while the sidebar is expanded. Its scrolling body has a bounded height to leave room for Workspaces. The whole Pinned section can be collapsed for the current page; view changes preserve that choice and reloading expands it. Creating a group or accepting a drop on the collapsed Pinned header expands the section.
|
|
37
|
+
|
|
38
|
+
### Reordering and view options
|
|
39
|
+
|
|
40
|
+
Pinned Sessions lead ordinary Sessions in both grouped and flat views. **Last updated** sorts each partition strictly by the latest user prompt or steer time, newest first; pin time does not affect it. **Manual** uses the relative positions in one complete Session sequence, including hidden archives. Returning to Last updated discards the manual layout, and entering Manual again freezes the then-current chronological order. The browser defaults to Last updated and remembers the selected mode across reloads.
|
|
41
|
+
|
|
42
|
+
Pinning moves the Session to the front of its complete saved sequence without changing the selected mode. It leads the pinned partition in Manual but need not lead it in Last updated. Unpinning leaves the saved position unchanged. Dragging either a pinned or ordinary row edits that same complete sequence and selects Manual; hidden archived members are retained. Missing pinned members prepend in pin-array order. New ordinary forks precede their sources in the complete sequence without inheriting pin membership; visible pins still lead ordinary rows. Other missing members append by recency with missing archives last. Supplementation stays in memory until a Session-order write records the complete result. Archive filtering alone changes neither saved positions nor account membership.
|
|
43
|
+
|
|
44
|
+
The selected blank **New Session** retains its provisional first slot and cannot be dragged; after its first prompt it becomes an ordinary draggable row, retaining that position in Manual or following its current timestamp in Last updated. A collapsed-group drag uses the target Session identity and keeps the source visible. Session display orders for real Workspaces, Ungrouped, and the flat list are browser-local; Workspace group drag order remains Host-durable.
|
|
45
|
+
|
|
46
|
+
### Workspace hierarchy
|
|
47
|
+
|
|
48
|
+
Choose **Add workspace** and select a directory to register it and open a Session. **View options → Group by** defaults to **WorkSpace**, which lists Workspaces as sibling sections. Select **Workspace Tree** to nest each Workspace under its nearest registered ancestor, including Workspaces added later. Each Workspace keeps its own Sessions and row actions. Child Workspaces appear before the parent's own Sessions. Ancestors start expanded unless a saved collapsed state exists. A saved collapse also hides the current Session; ancestor folder icons stay highlighted when a descendant Workspace contains it. Row fills and hit targets span the same width at every level; only the contents indent. Workspace dragging reorders siblings; dropping on a descendant targets the nearest compatible ancestor, so an expanded parent can be moved past without collapsing it. Search-result navigation expands every ancestor. Grouping and expansion are saved in the current browser; switching modes preserves each Workspace's expansion preference, and the single-list view stays flat.
|
|
49
|
+
|
|
50
|
+
Hierarchy uses registered canonical paths only. It does not scan for projects or resolve symlink aliases. Nesting does not change Session working directories, logs, or Workspace membership. Deleting a parent Workspace leaves its child Workspaces registered and places them under their next registered ancestor, or at the root.
|
|
51
|
+
|
|
52
|
+
### Search
|
|
53
|
+
|
|
54
|
+
Collapsed search is one header action beside the view and add actions: activating it expands the field across the header. A non-blank query replaces either browsing mode with one flat result list — case-insensitive title and Workspace substring matches appear immediately, while a 250 ms debounced Host request adds ranked current-conversation content matches and snippets. Each new query aborts the preceding request; a failed content search leaves metadata matches visible without an additional warning. The list is capped at 20. Choosing an unarchived result clears and collapses search, opens the Session, and scrolls its row into view in the configured browsing mode; grouped browsing also expands its Workspace and the full Session list when required. Archived results offer Unarchive; attempting to open one explains that restriction without clearing the query or navigating.
|
|
55
|
+
|
|
56
|
+
### Managing sessions
|
|
57
|
+
|
|
58
|
+
The Session row's Rename action opens a dialog prefilled with the row's display title; confirming an unchanged title is deliberately allowed — it pins the current automatic title against regeneration. Double-clicking a title also opens Rename; for an unarchived Session, the preceding clicks open its conversation. Rename uses a temporary `workspaceOperation` reference and awaits its initial history opening. The row's Fork action reads ordinary cold history normally. When the Host reports that source migration is required, an app-wide Toast offers to open that Session; the action only navigates and does not retry fork. A successful fork uses the source's last completed turn and increments the inherited persisted title through Session Controller without retaining the child, opening its history, or changing selection. Workspace Delete opens a confirmation that states the retention boundary; success removes the group while its Sessions remain under Ungrouped. Pin, Rename, Fork, and Archive are themselves entries of the `sidebar.workspaces.session.menu.item` list (pin and archive also of `sidebar.workspaces.session.row.action`), so a client plugin's action takes whatever position its `order` gives it.
|
|
59
|
+
|
|
60
|
+
Archive commits without a confirmation dialog for a quiet Session and retains the Session's account position. A Session with running work is the one case that asks first: the Host refuses the plain archive and names that work, and the sidebar opens a stop-and-archive dialog listing it by family — the turn in progress, running subagents, background jobs, scheduled reminders, each with its labels — with the restore path stated; confirming asks the Host to stop the work the way the stop button does and archives once the archive set is durable, while the stops settle in the background, and cancelling leaves the Session running and visible. View options control visibility through one explicit three-way choice: Hide archived (the default) hides archived Sessions, All conversations (show archived) includes them, and Archived only hides ordinary Sessions and drops Workspaces without an archived Session; in tree grouping, children of a dropped Workspace nest under the nearest shown ancestor. Visible archived rows are grayed and carry an accessible explanation that they cannot be opened until restored; Rename, Fork, and Unarchive remain available. A successful archive shows a notice with an undo action and — while archived rows are hidden — a "filter archived sessions" action that switches the filter to All conversations (show archived); a stop-and-archive shows the same notice under its own wording, and undo restores the Session without resuming the stopped work. Unarchive removes the archive mark without restoring a pin or changing the saved position. An empty list shows a centered glyph-over-text placeholder; the Archived-only view names its own emptiness (No archived sessions yet) and adds a View other sessions text action that switches the filter back to Hide archived.
|
|
61
|
+
|
|
62
|
+
Session update times use the tertiary label color, including on archived rows. A Session title wider than its row is clipped with an ellipsis at rest. Hovering the row scrolls the title to its far edge — the incremented title of a fork, for example — and reveals it without the ellipsis; leaving the row returns the title to its start.
|
|
63
|
+
|
|
64
|
+
The keyboard reference exposes New Session, Search sessions, Add workspace, Rename session, Fork session, and Archive session. Desktop defaults use the platform's primary modifier, with Mod+K for search; Windows and macOS Web use the [shortcut service’s platform defaults](../shortcuts/README.md); Linux Web leaves these commands unbound until configured. Button tooltips and Session row menus display the effective bindings. Clicking a menu item targets that row; pressing its shortcut targets the main Session. Desktop and Windows/macOS Web rename use Mod+Alt+G, leaving Mod+Shift+R to browser reload. Rename requires a nonblank main Conversation with no modal obscuring it; a retained Session behind another main panel is unavailable. Other Windows and macOS Desktop bindings also execute from terminal input and modal dialogs; other environments follow the command's region and modal restrictions. Search and rename requests belong to this package; input drafts remain in the browser. The directory picker rejects another open while selection or Workspace adoption is pending. Fork captures the retained main-view Session, for which navigation has requested history opening, and keeps automatic migration. It uses the row action's Host operation to select its last completed turn, without loading older Client history. Missing or blank Sessions are unavailable; the Host rejects a source with no completed turn. A rejected shortcut fork preserves the current selection, displays a localized notification, and can be retried; unexpected failures also retain diagnostic logging.
|
|
65
|
+
|
|
66
|
+
### Pending interactions
|
|
67
|
+
|
|
68
|
+
Session rows render the runtime's live `pendingInteraction` classification: approvals report **Waiting for approval**, plan reviews report **Plan awaiting review**, and ordinary questions report **Waiting for answer**. While an interaction is pending, the row uses the shared warning dot and replaces its trailing update time with **Approval**, **Plan review**, or **Answer**; the full status and relative time remain in hover details. Pending interaction takes precedence over the shared ongoing loader; a finished-but-unviewed Session uses done, while idle remains dot-free in the row and uses the shared idle dot in its hover details. The leading seat renders only while the row's primary status is idle — no pending interaction, no own or descendant activity, and no unviewed completion — so an occupant there never appears beside the row's own status dot. An archived row keeps that cell blank: it shows neither a status dot nor the seat, and its live status appears on the hover card only.
|
|
69
|
+
|
|
70
|
+
### Active Schedule markers
|
|
71
|
+
|
|
72
|
+
Grouped and flat Session rows show a clock mark when the Session has active scheduled tasks. It is an occupant of the row's `sidebar.session.row.leading` seat, so it renders only on a row whose primary status is idle and never beside the row's own status dot; an archived row keeps that cell blank, and a search result has no leading seat and shows no mark. It is not a button, has no tab stop, and a press on its area does not open the row. The mark's own read and the meaning of its active-task condition belong to [ui-schedule](../ui-schedule/README.md).
|
|
73
|
+
|
|
74
|
+
-----
|
|
75
|
+
|
|
76
|
+
`ctx.uiWorkspace.openSession(target)` synchronously replaces the owned `mainView` reference and returns the main area to Conversation without waiting for `reference.ready`, so history loading renders inside the selected Session view. The target may be a known Session id or a durable direct-parent subagent address; an explicit address does not require a preloaded parent catalog. `openWorkspace(id, beforeOpen?)` opens a Workspace blank only if no later navigation has superseded the request; an explicit New Session creates a fresh Session. `forkSession(id)` creates the child without navigating or superseding a pending navigation. The optional synchronous preparation callback runs after the target is retained and only for a current Workspace request, so superseded requests do not move composer drafts. Navigation or owner disposal suppresses the late UI commit, not the underlying Session creation. Startup restoration retains its main reference without changing the selected panel or cancelling a later navigation. Archiving the main Session releases its reference and clears the main selection. Selection failure leaves a global panel visible. Session rows read `usePanelInfo` to suppress their selected appearance while a global panel is active; search and directory-picker focus alone do not leave that panel.
|
|
77
|
+
|
|
78
|
+
Navigation and startup restoration obtain the selected Session's projections from its `follow`; neither issues a separate projection refresh for that Session or its parent.
|
|
79
|
+
|
|
80
|
+
Workspace reopening and explicit draft preparation try to acquire the first eligible blank in catalog order; startup restoration tries the saved blank. If that writer is held, navigation creates a new Session without trying other blanks. Other acquisition failures abort the request: an explicit New Session or hero Workspace pick shows the failure as a transient notice quoting the Host's code and message (for example a preset that fails to mount) or, for a failure that is not a Host refusal, that failure's own message; a request a later navigation or owner disposal superseded raises no notice, and startup restoration reports only to the console. Released blanks retain their slash-command state when reused. Later navigation cancels a pending startup selection.
|
|
81
|
+
|
|
82
|
+
On an empty installation, first use begins by choosing a real directory through the existing Workspace picker to register it and open a Session; this replacement does not call `workspaces.initializeDefault`. Existing Workspaces and Sessions use the ordinary startup restoration path with their current directories. The plugin does not migrate directories or rename stored Workspaces. A registered Workspace remains available if Session creation or later submission fails.
|
|
83
|
+
|
|
84
|
+
This package displays every stored Workspace title verbatim in sidebar rows and hover cards, search metadata, the hero picker menu, and rename and delete dialogs. The rename dialog uses that stored title as its initial value; confirming an unchanged draft does not rename it, and duplicate-title checks exclude the target by Workspace identity. The official Conversation composer independently localizes the historical title `default-workspace`; users can rename that Workspace to change its composer label. The plugin leaves Conversation and existing user data in place.
|
|
85
|
+
|
|
86
|
+
<a id="understand-the-implementation"></a>
|
|
87
|
+
## Understand the implementation
|
|
88
|
+
|
|
89
|
+
<details>
|
|
90
|
+
<summary>Implementation internals — click to expand</summary>
|
|
91
|
+
|
|
92
|
+
The package is one composition: both target slots are declared by other plugins, so `apply` uses `slots.inject()` to register for each declaration lifetime and re-register after a declaring slot is restored.
|
|
93
|
+
|
|
94
|
+
WorkspaceBrowser renders `sidebar.workspaces.sessionGroups` at its root before the Workspace header. SessionTree and FlatList continue to own their row reorder state and share the active Session id and synchronous `claimDrop` with WorkspaceBrowser. Claiming a Pinned drop prevents the source list from saving its earlier reorder marker. Drag end, source unmount, and view/filter changes clear only that source's handoff; search results remain non-draggable.
|
|
95
|
+
|
|
96
|
+
The browser entry also declares two root-scoped `list` child seats on each Session row: `sidebar.session.row.leading`, rendered only while that row's primary status is idle and left blank on an archived row, and `sidebar.session.row.hover`, mounted only while that row's hover card is open. Both take the row's Session identity and nothing else, so an occupant reads its own data by that id; a Session-scoped seat would force a Session binding, which would activate and retain every listed Session.
|
|
97
|
+
|
|
98
|
+
Client callers can pass `prompt` and `clearPreviousDraft` in the second argument to `uiWorkspace.startSession`. A prompt is plain text; structured drafts are handled internally during restoration and Workspace transfer. Without clearing, existing text or attachments are preserved; explicit clearing replaces only the target Session's text and inline references, leaving attachments intact. Option values are captured before asynchronous creation and applied to the exact retained binding before navigation commits. Superseded navigation does not write content; a submission-blocked input refuses preparation and keeps the previous selection. Calls without options create a fresh Session using the current Agent preset definition. Workspace reconnection and explicit draft preparation may reuse an existing blank Session. The [draft initialization decision](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.md) describes the input owner's data flow.
|
|
99
|
+
|
|
100
|
+
### The directory-flow hole
|
|
101
|
+
|
|
102
|
+
Each registration declares a **directory-flow child hole** (`single` kind: `conversation.hero.workspace.directoryFlow` / `sidebar.workspaces.directoryFlow`) that the composed picker package's client half fills with its picking interaction — the `-native` backend's renderless OS-chooser driver, an in-app browsing dialog under a `-browse` composition. The flat **Add workspace...** action renders only while the surface's hole is occupied; an empty hole means the composition has no picking affordance. The sidebar Add workspace action preserves an existing blank Session and its draft. This package owns the trigger and the adoption: the occupant reports one picked path per open through the hole's owner conversation (`open`/`busy`/`onPicked`/`onCancel`/`onError`), and the owner adopts it through the object layer, selecting the committed Workspace only after its list projection has refreshed.
|
|
103
|
+
|
|
104
|
+
### Session row actions
|
|
105
|
+
|
|
106
|
+
The Session row's "..." menu and its hover buttons are two `list` slots declared by the WorkspaceBrowser entry: `sidebar.workspaces.session.menu.item` and `sidebar.workspaces.session.row.action`. Every menu row and every hover button is an entry, this package's own actions included: `apply` registers `pin` (menu 100, button 200), `rename` (200), `fork` (300), and `archive` (menu 400, button 100) the way a client plugin registers its actions, so a plugin action lands wherever its `order` says, and reusing a shipped id at another `priority` shadows that action.
|
|
107
|
+
|
|
108
|
+
An entry receives only the row identity (`sessionId`, `displayTitle`) and owns everything else. It reads the Host state it cares about through its own injected hooks (the pin and archive sets, as Sets derived once per Workspace snapshot), decides its own visibility (pin renders nothing on an archived row, because the Host keeps the two sets exclusive), carries its whole behavior in its registration's `inject` face (a pin also fronts the Session in its saved orders; an archive raises the notice), and raises its own surfaces: the rename dialog, the stop-and-archive dialog, and the row-action notice are this package's `shell.overlay` entries, fed by the requests the actions inject. A menu entry renders one `role="menuitem"` button (this package's rows use `MenuItemButton` from ui-primitives, which adds the host styling and `separatorBefore` for a group start; the hairline comes and goes with the row) and dismisses the menu through the slot-level `useMenuOpenState` hook, the menu's own open state bound from the row's render occurrence; a hover button entry renders one icon button, and the strip keeps its clicks from opening the row. The browser passes no action callbacks to its rows; its remaining verbs are the search results' restore button and the title double-click, which raises the same rename request.
|
|
109
|
+
|
|
110
|
+
#### Packaged client plugin
|
|
111
|
+
|
|
112
|
+
Declare `ui-workspace`, `ui-slots`, `ui-renderer`, `client-locale`, and `ui-primitives` as browser/type development dependencies according to the Client dependency policy. The type-only `ui-workspace/client` import loads this package's `SlotMap` declaration; without it, an independently compiled plugin does not know the slot keys. Keep the Component at module scope and own visible copy in the contributing package's locale namespace.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
116
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
117
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
118
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
119
|
+
import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
|
|
120
|
+
import { MenuItemButton } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
121
|
+
import type {
|
|
122
|
+
InjectFace, LocaleDictOf, PropsLocale, PropsRuntime,
|
|
123
|
+
} from '@deepseek-ai/dsh-client-ui-slots'
|
|
124
|
+
import { exportSession } from './export-session.ts'
|
|
125
|
+
|
|
126
|
+
const NS = 'acme.sessionActions'
|
|
127
|
+
|
|
128
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
129
|
+
interface LocaleNamespaceMap {
|
|
130
|
+
'acme.sessionActions': 'export'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const en: LocaleDictOf<typeof NS> = { export: 'Export {title}' }
|
|
135
|
+
const zh: LocaleDictOf<typeof NS> = { export: '导出 {title}' }
|
|
136
|
+
|
|
137
|
+
interface ExportRowInjected {
|
|
138
|
+
exportSession: (sessionId: SessionId) => void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type ExportRowProps =
|
|
142
|
+
PropsRuntime<'sidebar.workspaces.session.menu.item'>
|
|
143
|
+
& PropsLocale<typeof NS>
|
|
144
|
+
& InjectFace<ExportRowInjected>
|
|
145
|
+
|
|
146
|
+
function ExportRow({ sessionId, displayTitle, useMenuOpenState, exportSession, t }: ExportRowProps) {
|
|
147
|
+
const [, setMenuOpen] = useMenuOpenState()
|
|
148
|
+
return (
|
|
149
|
+
<MenuItemButton separatorBefore onSelect={() => { setMenuOpen(false); exportSession(sessionId) }}>
|
|
150
|
+
{t('export', { title: displayTitle })}
|
|
151
|
+
</MenuItemButton>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const inject = ['slots', 'locale']
|
|
156
|
+
|
|
157
|
+
export function apply(ctx: Context): void {
|
|
158
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'acme-session-actions: dictionaries')
|
|
159
|
+
|
|
160
|
+
// Order 500 places the row after the shipped Archive (400); `separatorBefore` opens the plugin group.
|
|
161
|
+
ctx.slots.inject('sidebar.workspaces.session.menu.item', () => ctx.slots.register({
|
|
162
|
+
name: 'sidebar.workspaces.session.menu.item',
|
|
163
|
+
id: 'acme.export-session',
|
|
164
|
+
order: 500,
|
|
165
|
+
locale: NS,
|
|
166
|
+
inject: (): ExportRowInjected => ({ exportSession }),
|
|
167
|
+
}, ExportRow))
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`ctx.slots.inject()` is required even if the owner is normally present: it waits for the declaration, removes the contribution when that declaration collapses, and registers it again after restoration. The registration's `inject` factory may close over the plugin's declared Cordis services; the Component receives only the projected data and callbacks. A hover button registers into `sidebar.workspaces.session.row.action` the same way and renders one icon button.
|
|
172
|
+
|
|
173
|
+
#### Dynamic client package
|
|
174
|
+
|
|
175
|
+
A dynamically loaded browser half follows the same component contract; which modules it can reach depends on its lane. A Module Loader package (`factory(require)`, as in the real Loader/Web fixture) gets `@deepseek-ai/dsh-client-ui-primitives` as an implicit baseline external: resolve `MenuItemButton` through the loader's `require`, do not list the primitive as a runtime dependency or bundle another copy, and declare a development dependency only when source compilation needs its types. A `cordis-client-runner` closure (the audience of the generated Client Slot catalog) cannot import anything: it renders its own `role="menuitem"` `<button>` with `React.createElement`, styles it through `styles.insert`, and dismisses the menu through the same `useMenuOpenState` hook, as the catalog's example shows.
|
|
176
|
+
|
|
177
|
+
### View state
|
|
178
|
+
|
|
179
|
+
Once the Workspace baseline is ready, browser-persisted expansion and Session-order records retain only current Workspace ids plus Ungrouped and the flat-list account. `WorkspaceView.sessionIds` supplies real-Workspace membership, not Session display order. View actions receive complete account orders, never filtered rows. A new member without a Session summary waits for that summary, while a saved position survives a temporarily missing summary. Archive visibility is applied only when deriving rows. Pin and drag writes save complete orders; ordinary derivation does not write them. The selected blank Session remains an explicit position write, including during Workspace reconnection, when other saved members are retained until the baseline establishes membership. Ordering remains mounted while the sidebar is a rail or search replaces its body. Last updated derives from current summaries without reading saved positions; equal timestamps use Session ids as a stable tie-break.
|
|
180
|
+
|
|
181
|
+
The sidebar hides durable summaries with `origin: 'subagent'`. A visible ordinary row shows the shared ongoing loader from running direct children in its loaded parent catalog, never from summary lineage. Child activity uses the latest UI status, falling back to the Session summary until that status is known.
|
|
182
|
+
|
|
183
|
+
Row motion belongs to [AnimatedRows](src/client/rows/AnimatedRows.tsx). It measures keyed rows before and after React commits that change their membership or order, and uses native movement and opacity animations. Removed rows fade as inert copies outside the scrolling list; they do not delay React unmounting or extend its scroll range. Initial loading, drag commits, overflow expansion, and view-option changes settle immediately. The animator has no layout observers or polling and does not measure content-only updates or scrolling.
|
|
184
|
+
|
|
185
|
+
### Hover cards
|
|
186
|
+
|
|
187
|
+
Workspace and Session hover cards copy the value their row clips: activating a Workspace card writes its full directory path, while activating a non-blank Session card writes its full display title. A provisional blank New Session card remains read-only because its localized label is a placeholder rather than session content. A Session card also renders its `sidebar.session.row.hover` seat between the relative time and the trailing status lines whenever the card is open, independent of the row's own state.
|
|
188
|
+
|
|
189
|
+
</details>
|
|
190
|
+
|
|
191
|
+
-----
|
|
192
|
+
|
|
193
|
+
<a id="further-exploration"></a>
|
|
194
|
+
## Further Exploration
|
|
195
|
+
|
|
196
|
+
These pages cover the sidebar host, the hero surface, and the picking backends.
|
|
197
|
+
|
|
198
|
+
- [ui-sidebar](../ui-sidebar/README.md) — the sidebar shell hosting the `sidebar.workspaces` hole.
|
|
199
|
+
- [ui-conversation](../ui-conversation/README.md) — the chat surface hosting the Session Intent hero's picker hole.
|
|
200
|
+
- [directory-picker-native](../../host/directory-picker-native/README.md) — the OS-chooser backend filling the directory-flow hole.
|
|
201
|
+
- [Workspace Controller](../../api/workspace-controller/README.md) — the Host mutations and framework-neutral Client projection that own Workspaces, membership, and Workspace group order.
|
|
202
|
+
|
|
203
|
+
-----
|
|
204
|
+
|
|
205
|
+
<a id="model-experience"></a>
|
|
206
|
+
## Model Experience
|
|
207
|
+
|
|
208
|
+
None, as the package is a browser-side UI plugin layer that registers nothing model-facing.
|
|
209
|
+
|
|
210
|
+
#### KV Cache effect
|
|
211
|
+
|
|
212
|
+
None; this package neither assembles nor sends a provider request.
|
|
213
|
+
|
|
214
|
+
## Known Limitations and Deferred Work
|
|
215
|
+
|
|
216
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
These limits define the search depth, the archive surface, and the picking carrier; they are current package constraints.
|
|
220
|
+
|
|
221
|
+
- **No fuzzy content search or event deep links** — the content backend uses literal token/phrase matching, and selecting a result opens the Session rather than the matching event.
|
|
222
|
+
- **No Session deletion** — sessions can be archived but never deleted; archived rows stay recoverable in place through the archived view filter and the search results' unarchive action, and Workspace registration deletion does not delete Sessions.
|
|
223
|
+
- **Pending user interaction is not aggregated into collapsed groups** — a waiting row inside a collapsed group lights no group-header indicator and becomes visible only after that group is expanded.
|
|
224
|
+
- **Native folder selection depends on the local Host carrier** — under the `-native` composition, in-process or remote browser deployments cannot open a local operating-system dialog; remote-capable picking is the `-browse` composition's in-app flow.
|
|
225
|
+
|
|
226
|
+
<a id="dev-note"></a>
|
|
227
|
+
### Dev Note
|
|
228
|
+
|
|
229
|
+
<details>
|
|
230
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
231
|
+
|
|
232
|
+
None.
|
|
233
|
+
|
|
234
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "带可选 Session 分组 slot、与指定版本绑定的 dsh Web Workspace 替代插件。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @umialpha/dsh-client-ui-workspace-groups
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
本包 fork 自 DeepSeek Harness 的 `@deepseek-ai/dsh-client-ui-workspace`,基线为 tag `dsh-v0.2.1-alpha.2`(commit `d743267388641bc76f17c45ce8b4c231aed1d32c`)。`0.1.0` 只支持 DSH `0.2.1-alpha.2`;它在 Workspace 浏览界面中增加可选的 `sidebar.workspaces.sessionGroups` slot。复制的上游代码仍归 Copyright (c) 2026 DeepSeek,并按本包内的 MIT License 发布。
|
|
11
|
+
|
|
12
|
+
桌面端产品事件使用可选的[产品埋点服务](../product-analytics/README.zh.md),不包含普通 Web 交互。
|
|
13
|
+
|
|
14
|
+
## 概述
|
|
15
|
+
|
|
16
|
+
本包让用户浏览分组或扁平的 Session 列表、为新 Session 选择 Workspace,并通过添加、重命名、重排序、搜索、fork、归档和删除 Workspace 来管理 Workspace 与 Session;Session 行菜单及其悬停按钮是可由客户端插件扩展的 slot 列表。待处理交互显示为警告点,subagent 来源的 Session 则保持隐藏。处于空闲状态且未归档、其 Session 有活动定时任务的 Session 行会显示时钟标记,其悬浮卡片会列出这些任务。规范化后仍有差异的文件夹路径会保留为独立 Workspace。添加 Workspace 需要组合目录选择器;没有目录选择器时,添加操作不可用。
|
|
17
|
+
|
|
18
|
+
## 目录
|
|
19
|
+
|
|
20
|
+
- [使用本包](#use-this-package)
|
|
21
|
+
- [理解实现](#understand-the-implementation)
|
|
22
|
+
- [进一步探索](#further-exploration)
|
|
23
|
+
- [模型体验](#model-experience)
|
|
24
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
25
|
+
- [开发备注](#dev-note)
|
|
26
|
+
|
|
27
|
+
-----
|
|
28
|
+
|
|
29
|
+
<a id="use-this-package"></a>
|
|
30
|
+
## 使用本包
|
|
31
|
+
|
|
32
|
+
用侧边栏浏览 Workspace 及其 Session、重排它们并新建会话;在 Session Intent 主视觉区用选择器为新会话选择 Workspace。打开的 Workspace 默认显示五条空闲的非空白 Session。正在运行的 Session(包括有子会话正在运行的父会话)始终按原顺序显示,不占用这五条配额;当前选中的空白**新会话**在首条提示词落地前也作为额外行。每次点击**展开其余**最多再显示五条空闲 Session;全部显示后,**收起**恢复初始行数,但仍显示正在运行的 Session。关闭再打开 Workspace 也会恢复该折叠投影。
|
|
33
|
+
|
|
34
|
+
未保存标题的历史会话显示本地化名称「未命名 / Untitled」,不使用目录名兜底。当前空会话仍显示「新会话」,其他空会话仍隐藏。重命名草稿使用已保存标题,无标题时留空;无标题行不提供标题复制。
|
|
35
|
+
|
|
36
|
+
组合 Session groups Client 后,“置顶”和“工作区”是侧边栏中的同级区块。置顶位于工作区标题上方,在侧边栏展开时持续显示于分组、目录树、单列表和搜索视图中。置顶主体限制滚动高度,为工作区保留空间。整体折叠只作用于当前页面;切换视图保留折叠选择,刷新后恢复展开。创建分组或在折叠的置顶标题上接受拖放后,置顶区会展开。
|
|
37
|
+
|
|
38
|
+
### 重排序与视图选项
|
|
39
|
+
|
|
40
|
+
分组和平铺视图都先显示置顶 Session,再显示普通 Session。**最近更新**在各分区内严格按最近一次用户提示词或 steering(中途引导)时间降序排列,置顶时间不影响它。**手动排序**使用同一条完整 Session 序列中的相对位置,包括隐藏的归档项。返回最近更新会丢弃手动布局,再次进入手动排序时冻结当时的时间顺序。浏览器默认采用最近更新,并在重新加载后记住所选模式。
|
|
41
|
+
|
|
42
|
+
置顶把 Session 移到完整保存序列的首位,但不切换所选模式。它在手动模式下排在置顶区最前,在最近更新模式下则不一定。取消置顶不改变保存的位置。拖拽置顶行或普通行都修改同一条完整序列并选择手动模式,隐藏的归档成员始终保留。缺失的置顶成员按置顶数组顺序补到头部。新增的普通 fork 在完整序列中插在来源之前,不继承置顶成员关系;可见置顶行仍排在普通行之前。其他缺失成员按最近更新时间追加,缺失归档项放在最后。补齐仅在内存中完成,直到一次 Session 顺序写入保存完整结果。单纯切换归档筛选既不改变保存的位置,也不改变成员关系。
|
|
43
|
+
|
|
44
|
+
当前选中的空白**新会话**保留临时首位且无法拖拽;首条提示词落地后,它成为可拖拽的普通行,在手动排序中保留该位置,在最近更新中按当前时间戳排列。折叠分组的拖拽使用目标 Session 身份,并保持来源行可见。真实 Workspace、Ungrouped 与单列表的 Session 显示顺序都保留在浏览器本地;Workspace 分组的拖拽顺序仍由 Host 持久化。
|
|
45
|
+
|
|
46
|
+
### 工作区层级
|
|
47
|
+
|
|
48
|
+
选择**添加工作区**并选取目录,即可注册工作区并打开 Session。**视图选项 → 分组方式**默认为**按工作区**,将工作区作为同级分组显示。选择**按工作区树**后,每个 Workspace 会位于最近的已注册祖先之下,之后添加的 Workspace 也会自动归入。每个 Workspace 保留自己的 Session 和行操作,子 Workspace 显示在父级自己的 Session 之前。祖先默认展开,已有的折叠偏好除外。保存的折叠状态也会隐藏当前 Session;如果后代 Workspace 包含当前 Session,祖先文件夹图标仍保持高亮。各层级的高亮和点击区域保持整行同宽,仅内容缩进。拖拽 Workspace 仅重排同级项目;落在后代行上时,由最近的兼容祖先接收,因此无需先折叠父级就能将其他工作区拖到其后。选择搜索结果会展开全部祖先。分组方式和展开状态保存在当前浏览器中;切换模式会保留各 Workspace 的展开偏好,单列表视图保持平铺。
|
|
49
|
+
|
|
50
|
+
层级仅使用已注册的规范路径,不扫描项目,也不解析符号链接别名。嵌套不会改变 Session 的工作目录、日志或 Workspace 归属。删除父 Workspace 后,子 Workspace 仍保持注册,并归入下一个已注册祖先;没有祖先时显示在根层级。
|
|
51
|
+
|
|
52
|
+
### 搜索
|
|
53
|
+
|
|
54
|
+
折叠搜索是视图和添加操作旁的一枚区头按钮:激活后输入框会扩展并占据区头。非空白查询会以单一扁平结果列表替代任一浏览模式——不区分大小写的标题和 Workspace 子串匹配项会立即显示,经 250 ms 防抖的 Host 请求则会加入经过排序的当前对话内容匹配项及其摘要片段。每次新查询都会中止前一个请求;内容搜索失败时,元数据匹配项仍会显示,不另给警告。列表最多显示 20 条结果。选择未归档结果会清空并收起搜索、打开 Session,并在当前浏览模式中将其行滚动到可见区域;分组浏览还会按需展开所属 Workspace 和完整 Session 列表。已归档结果提供取消归档操作;尝试打开时会说明限制,不清空查询,也不导航。
|
|
55
|
+
|
|
56
|
+
### 管理会话
|
|
57
|
+
|
|
58
|
+
Session 行内的 Rename 操作打开一个以该行显示标题预填的对话框;确认未修改的标题是有意允许的——这正是把当前自动标题钉住、不再被重新生成覆盖的手势。双击标题也会打开 Rename;对于未归档 Session,先发生的点击会打开其对话。Rename 使用临时 `workspaceOperation` reference,并等待首次历史打开。行内 Fork 正常读取普通冷历史。Host 报告源会话需要迁移时,应用级 Toast 提供打开该会话的操作;该操作仅导航,不重试 fork。成功的 fork 在源会话最后一个已完成轮次处创建分支,通过 Session Controller 递增继承的持久化标题,不 retain 子会话、不打开其历史,也不改变选择。Workspace 行内的 Delete 操作会打开确认框,说明保留边界;成功后该分组被移除,其 Session 则留在 Ungrouped 下。Pin、Rename、Fork、Archive 本身就是 `sidebar.workspaces.session.menu.item` 列表的条目(pin 与 archive 同时也是 `sidebar.workspaces.session.row.action` 的条目),因此客户端插件的 action 由其 `order` 决定落在哪个位置。
|
|
59
|
+
|
|
60
|
+
对静止的 Session,Archive 不经确认对话框直接提交,并保留 Session 的记账位置。仍有工作在跑的 Session 是唯一会先询问的情形:Host 拒绝普通归档并列出这些工作,侧栏随即打开"停止并归档"对话框,按族列出——进行中的回合、运行中的子代理、后台任务、定时提醒,各带名称——并写明恢复路径;确认后请 Host 按停止按钮同样的方式停止这些工作,归档集合持久化后即完成归档,停止在后台收敛;取消则让 Session 继续运行并保持可见。视图选项以一组显式三选一控制显隐:隐藏已归档(默认项)隐藏已归档 Session,全部对话(显示已归档)将其纳入列表,仅显示已归档则隐藏普通 Session,并丢弃没有归档 Session 的 Workspace;树形分组下,被丢弃 Workspace 的子级挂到最近一个仍显示的祖先下。可见的归档行置灰,并提供无障碍说明,告知取消归档后才能打开;Rename、Fork 与取消归档仍然可用。归档成功后的提示提供"撤销"动作,并在归档行仍被隐藏时附带"筛选已归档会话"动作,后者直接把筛选切到全部对话(显示已归档);停止并归档显示同样的提示但措辞不同,撤销只恢复 Session,不会让被停止的工作继续。取消归档移除归档标记,但不恢复置顶,也不改变保存的位置。列表为空时显示居中的"图标在上、文字在下"占位;仅显示已归档视图用自己的文案(暂无已归档会话),并附"查看其他会话"文字按钮,点击把筛选切回隐藏已归档。
|
|
61
|
+
|
|
62
|
+
会话更新时间使用 tertiary 文本色,包括已归档行。标题宽于所在行时,静止状态以省略号裁切。把指针停在行上,标题会滚动到远端——例如 fork 递增后的标题——并在揭示时不显示省略号;指针离开后标题回到开头。
|
|
63
|
+
|
|
64
|
+
快捷键速查提供新建会话、搜索会话、添加工作区、重命名会话、分叉会话和归档会话。桌面默认使用平台的主修饰键,搜索为 Mod+K;Windows 和 macOS Web 使用[快捷键服务的平台默认值](../shortcuts/README.zh.md);Linux Web 在用户配置前不绑定这些命令。按钮提示和会话行菜单显示当前有效绑定。点击菜单项操作该行,按快捷键操作主会话。Desktop 和 Windows/macOS Web 的重命名均使用 Mod+Alt+G,将 Mod+Shift+R 留给浏览器刷新。重命名要求主区域正在显示非空会话,且没有模态对话框遮挡;切换到其他主面板后,后台保留的会话不可作为重命名目标。其他 Windows 和 macOS Desktop 绑定也可从终端输入区域和模态对话框中执行;其他环境遵循命令的区域和模态限制。搜索和重命名的打开请求归本包管理,输入草稿保留在浏览器中。目录选择或工作区接纳尚未结束时,目录选择器拒绝重复打开。分叉捕获主视图已持有并发起历史打开的会话,保留自动迁移;它使用与行菜单相同的 Host 操作选择最近已完成轮次,不读取更早的 Client 历史。没有会话或会话为空时不可用;没有已完成轮次的源会话由 Host 拒绝。快捷键分叉被拒绝时保留当前选择、显示本地化提示,并允许重试;非预期失败还会保留诊断日志。
|
|
65
|
+
|
|
66
|
+
### 待处理交互
|
|
67
|
+
|
|
68
|
+
Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示**等待审批**,计划审阅显示**计划待审**,普通问题显示**等待回答**。交互待处理期间,该行使用共享 warning 橙点,并以**待批准**、**计划待审**或**待回答**替换尾部更新时间;悬停详情仍保留完整状态和相对时间。待处理交互的优先级高于共享 ongoing loading;已完成但未查看的 Session 使用 done,idle 在行内不显示点,在悬停详情中使用共享 idle 灰点。 行首 seat 仅在该行的主状态为 idle 时渲染——没有待处理交互、没有自身或后代活动、也没有未查看的完成提醒——因此该处的占用方不会与行自身的状态点同时出现。归档行该格整体留空:既不显示状态点也不挂 seat,其活动状态只出现在悬浮卡片上。
|
|
69
|
+
|
|
70
|
+
### 活动 Schedule 标识
|
|
71
|
+
|
|
72
|
+
当成组的与平铺的 Session 行所对应的 Session 存在活动定时任务时,该行显示时钟标记。它是行首 `sidebar.session.row.leading` seat 的占用方,因此仅在该行主状态为 idle 时渲染,绝不与行自身的状态点同时出现;归档行该格同样留空,搜索结果没有该 seat,也不显示标记。它不是按钮,没有 Tab stop,点击其所在区域不会打开整行。标记自身的读取及其「活动」条件的确切含义由 [ui-schedule](../ui-schedule/README.zh.md) 负责。
|
|
73
|
+
|
|
74
|
+
-----
|
|
75
|
+
|
|
76
|
+
`ctx.uiWorkspace.openSession(target)` 会同步替换其拥有的 `mainView` reference,并让主区域返回 Conversation,而不等待 `reference.ready`,因此历史加载会显示在已经选中的 Session 视图内。目标可以是已知 Session id,也可以是持久的直接父子 subagent 地址;显式地址不要求预先加载 parent catalog。`openWorkspace(id, beforeOpen?)` 仅在请求未被后续导航替代时打开工作区空白 Session;显式新会话操作会创建新的 Session。`forkSession(id)` 创建子会话,不导航,也不替代尚未完成的导航。可选的同步准备回调在目标被 retain 后执行,并且仅对仍有效的 Workspace 请求执行,因此过期请求不会搬移 composer 草稿。后续导航或 owner 释放会阻止晚到的 UI 提交,但不取消底层 Session 创建。启动恢复会 retain 主 reference,不改变已选面板,也不取消后续导航。归档主 Session 会释放其 reference 并清除主选择。选择失败时保留当前全局面板。Session 行读取 `usePanelInfo`,在全局面板活跃时不显示 Session 选中样式;仅把焦点移到搜索框或目录选择器不会离开该面板。
|
|
77
|
+
|
|
78
|
+
导航和启动恢复通过所选 Session 的 `follow` 获取投影,不会另行刷新该 Session 或其父会话的投影。
|
|
79
|
+
|
|
80
|
+
重新打开工作区和显式准备草稿尝试获取列表中第一个符合条件的空白会话;启动恢复尝试已保存的空白会话。若该写锁被占用,导航直接新建 Session,不再尝试其他空白会话。其他获取错误会中止请求:显式新建会话或在 hero 中选择工作区时,失败以短暂提示展示,引用 Host 的错误码和消息(例如 preset 挂载失败),非 Host 拒绝的失败则显示其自身消息;已被后续导航或 owner 销毁取代的请求不弹提示,启动恢复仍只报告到控制台。已释放的空白会话被复用时保留 slash 命令状态。后续导航会取消尚未完成的启动选择。
|
|
81
|
+
|
|
82
|
+
空安装环境首次使用时,通过现有 Workspace 选择器选择真实目录,登记后打开 Session;本替代插件不调用 `workspaces.initializeDefault`。已有 Workspace 与 Session 沿用普通启动恢复流程,继续使用原目录。插件不迁移目录,也不重命名已保存的 Workspace。登记成功的 Workspace 在 Session 创建或后续提交失败时仍然保留。
|
|
83
|
+
|
|
84
|
+
本包在侧栏行及其悬浮卡片、搜索元信息、hero 选择器菜单、重命名与删除对话框中均显示 Workspace 的存储标题原文。重命名对话框以该存储标题为初值,确认未改动的草稿不会重命名;重名检查按 Workspace 身份排除目标自身。官方 Conversation 的输入区仍独立本地化历史标题 `default-workspace`;用户可以自行重命名该 Workspace 来改变输入区标签。插件保留现有 Conversation 与用户数据。
|
|
85
|
+
|
|
86
|
+
<a id="understand-the-implementation"></a>
|
|
87
|
+
## 理解实现
|
|
88
|
+
|
|
89
|
+
<details>
|
|
90
|
+
<summary>实现细节——点击展开</summary>
|
|
91
|
+
|
|
92
|
+
本包是一条组合:两个目标 slot 都由其他插件声明,因此 `apply` 使用 `slots.inject()` 在各自的声明生命周期内完成注册,并在目标 slot 的声明恢复后重新注册。
|
|
93
|
+
|
|
94
|
+
WorkspaceBrowser 在根容器、工作区标题之前渲染 `sidebar.workspaces.sessionGroups`。SessionTree 与 FlatList 继续拥有行重排状态,向 WorkspaceBrowser 交接当前 Session id 与同步 `claimDrop`。置顶区接管 drop 后,源列表不会保存此前的重排标记。拖放结束、源列表卸载或视图/筛选切换只清空本源的交接;搜索结果仍不可拖动。
|
|
95
|
+
|
|
96
|
+
浏览器入口还为每个 Session 行声明两个 root 作用域的 `list` 子 slot:`sidebar.session.row.leading` 仅在该行主状态为 idle 时渲染、归档行留空,`sidebar.session.row.hover` 仅在该行的悬浮卡片打开时挂载。两者只接收行的 Session 身份,占用方据此读取自己的数据;Session 作用域的 slot 会强制建立 Session 绑定,从而激活并保留列表中每个 Session。
|
|
97
|
+
|
|
98
|
+
客户端调用方可以在 `uiWorkspace.startSession` 的第二参数中传入 `prompt` 与 `clearPreviousDraft`。提示词仅接受普通文字;结构化草稿由内部恢复和工作区携带流程处理。不清空时保留已有文字或附件;显式清空只替换目标 Session 的文字和内联引用,不删除附件。在异步创建前捕获选项值,在导航提交前应用到准确保留的 binding。已被后续操作取代的导航不写内容;输入处于提交阻塞状态时拒绝准备并保留原选择。不传选项时按当前 Agent 预设定义创建新的 Session。工作区重连和显式草稿准备仍可复用已有空 Session。[草稿初始化决策](../../../.agents/notes/implemented/architecture/2026-09-30-structured-draft-initialization.zh.md)说明输入 owner 的数据流。
|
|
99
|
+
|
|
100
|
+
### 目录流子 slot
|
|
101
|
+
|
|
102
|
+
每个注册各自声明一个**目录流子 slot**(`single` kind:`conversation.hero.workspace.directoryFlow`/`sidebar.workspaces.directoryFlow`),由组合的选择器包 client half 填入其选取交互——`-native` 后端的无渲染 OS 选择器驱动,`-browse` 组合下则是应用内浏览对话框。平铺显示的**添加工作区…** 操作仅在当前界面的 slot 被占用时渲染;slot 为空意味着该组合没有目录选择能力。侧栏的添加工作区操作保留已有空白 Session 及其草稿。本包持有触发与接纳:占用方通过 slot 的属主交互约定(`open`/`busy`/`onPicked`/`onCancel`/`onError`)每次打开上报一个所选路径,owner 通过对象层接纳它,并等待 Workspace 列表投影刷新后才选中已提交的 Workspace。
|
|
103
|
+
|
|
104
|
+
### Session 行 action
|
|
105
|
+
|
|
106
|
+
Session 行的 "..." 菜单和行尾悬停按钮是 WorkspaceBrowser 注册项声明的两个 `list` slot:`sidebar.workspaces.session.menu.item` 与 `sidebar.workspaces.session.row.action`。每一个菜单行、每一个悬停按钮都是条目,本包自己的 action 也不例外:`apply` 以客户端插件注册自己 action 的同一方式注册 `pin`(菜单 100、按钮 200)、`rename`(200)、`fork`(300)、`archive`(菜单 400、按钮 100),因此插件 action 落在其 `order` 所指的位置,以另一个 `priority` 复用内置 id 则遮蔽该 action。
|
|
107
|
+
|
|
108
|
+
条目只接收行身份(`sessionId`、`displayTitle`),其余一切自己负责:用自己注入的 hook 读自己关心的 Host 状态(置顶与归档集合,以每次 Workspace 快照只派生一次的 Set 形式),自己决定是否显示(Host 规定归档与置顶互斥,所以 pin 在归档行上不渲染),整套行为放在注册项自己的 `inject` face 里(置顶成功后顺带把会话推到保存顺序最前,归档成功后发提示),浮层也自己带——重命名对话框、停止并归档对话框和行 action 的提示是本包注册在 `shell.overlay` 的条目,由 action 注入的请求驱动。菜单条目渲染一个 `role="menuitem"` 的按钮(本包自己的行用 ui-primitives 的 `MenuItemButton`,它带宿主样式,开启新分组的行加 `separatorBefore`,分隔线随行一起出现和消失),并通过 slot 级 `useMenuOpenState` hook(菜单自身的打开状态,从该行的渲染出现处绑定)关闭菜单;悬停按钮条目渲染一个图标按钮,按钮条会拦住点击、不让它打开该行。browser 不再向行传任何 action 回调,它剩下的动作只有搜索结果里的恢复按钮和标题双击,后者发出的是同一个重命名请求。
|
|
109
|
+
|
|
110
|
+
#### 打包客户端插件
|
|
111
|
+
|
|
112
|
+
按照 Client 依赖规则,将 `ui-workspace`、`ui-slots`、`ui-renderer`、`client-locale` 与 `ui-primitives` 声明为浏览器/类型开发依赖。纯类型的 `ui-workspace/client` import 会加载本包的 `SlotMap` 声明;缺少该 import 时,独立编译的插件不会知道这些 slot key。Component 保持模块级稳定身份;用户可见文案由贡献包自己的 locale namespace 持有。
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
116
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
117
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
118
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
119
|
+
import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
|
|
120
|
+
import { MenuItemButton } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
121
|
+
import type {
|
|
122
|
+
InjectFace, LocaleDictOf, PropsLocale, PropsRuntime,
|
|
123
|
+
} from '@deepseek-ai/dsh-client-ui-slots'
|
|
124
|
+
import { exportSession } from './export-session.ts'
|
|
125
|
+
|
|
126
|
+
const NS = 'acme.sessionActions'
|
|
127
|
+
|
|
128
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
129
|
+
interface LocaleNamespaceMap {
|
|
130
|
+
'acme.sessionActions': 'export'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const en: LocaleDictOf<typeof NS> = { export: 'Export {title}' }
|
|
135
|
+
const zh: LocaleDictOf<typeof NS> = { export: '导出 {title}' }
|
|
136
|
+
|
|
137
|
+
interface ExportRowInjected {
|
|
138
|
+
exportSession: (sessionId: SessionId) => void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type ExportRowProps =
|
|
142
|
+
PropsRuntime<'sidebar.workspaces.session.menu.item'>
|
|
143
|
+
& PropsLocale<typeof NS>
|
|
144
|
+
& InjectFace<ExportRowInjected>
|
|
145
|
+
|
|
146
|
+
function ExportRow({ sessionId, displayTitle, useMenuOpenState, exportSession, t }: ExportRowProps) {
|
|
147
|
+
const [, setMenuOpen] = useMenuOpenState()
|
|
148
|
+
return (
|
|
149
|
+
<MenuItemButton separatorBefore onSelect={() => { setMenuOpen(false); exportSession(sessionId) }}>
|
|
150
|
+
{t('export', { title: displayTitle })}
|
|
151
|
+
</MenuItemButton>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const inject = ['slots', 'locale']
|
|
156
|
+
|
|
157
|
+
export function apply(ctx: Context): void {
|
|
158
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'acme-session-actions: dictionaries')
|
|
159
|
+
|
|
160
|
+
// Order 500 places the row after the shipped Archive (400); `separatorBefore` opens the plugin group.
|
|
161
|
+
ctx.slots.inject('sidebar.workspaces.session.menu.item', () => ctx.slots.register({
|
|
162
|
+
name: 'sidebar.workspaces.session.menu.item',
|
|
163
|
+
id: 'acme.export-session',
|
|
164
|
+
order: 500,
|
|
165
|
+
locale: NS,
|
|
166
|
+
inject: (): ExportRowInjected => ({ exportSession }),
|
|
167
|
+
}, ExportRow))
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
即使 owner 通常已经存在,也必须使用 `ctx.slots.inject()`:它等待声明,在声明折叠时移除贡献项,并在声明恢复后重新注册。注册项的 `inject` factory 可以闭包使用插件已声明的 Cordis service;Component 只接收投影后的数据和 callback。悬停按钮以同样方式注册到 `sidebar.workspaces.session.row.action`,渲染一个图标按钮。
|
|
172
|
+
|
|
173
|
+
#### 动态客户端包
|
|
174
|
+
|
|
175
|
+
动态加载的 browser half 采用同一套组件协议,能拿到哪些模块取决于它走哪条 lane。Module Loader 包(`factory(require)`,即真实 Loader/Web fixture 那种)把 `@deepseek-ai/dsh-client-ui-primitives` 当作隐式 baseline external:通过 loader 的 `require` 解析 `MenuItemButton`,不要把 primitive 列为运行时依赖或打包另一份副本,仅在源码编译需要其类型时声明开发依赖。`cordis-client-runner` 闭包(生成的 Client Slot catalog 面向的读者)无法 import 任何东西:它用 `React.createElement` 渲染自己的 `role="menuitem"` `<button>`,样式经 `styles.insert` 注入,并通过同一个 `useMenuOpenState` hook 关闭菜单,catalog 里的示例就是这个写法。
|
|
176
|
+
|
|
177
|
+
### 视图状态
|
|
178
|
+
|
|
179
|
+
Workspace 基线就绪后,浏览器持久化的展开状态和 Session 顺序记录只保留当前 Workspace id、Ungrouped 和单列表记账。`WorkspaceView.sessionIds` 提供真实 Workspace 的成员关系,而不提供 Session 显示顺序。视图操作接收完整记账顺序,而不是筛选后的行。尚无 Session 摘要的新成员会等待摘要,已保存的位置则在摘要暂时缺失时保留。归档显隐仅在派生行时应用。置顶和拖拽写入完整顺序,普通派生不执行写入。当前选中的空白 Session 仍是一次显式位置写入;Workspace 重连时同样如此,此时保留其他已保存成员,直到基线确定成员关系。侧边栏收成窄栏或搜索替代列表主体时,排序仍保持挂载。最近更新从当前摘要派生,不读取已保存位置;时间相同时按 Session id 稳定排序。
|
|
180
|
+
|
|
181
|
+
侧边栏隐藏持久化摘要中带有 `origin: 'subagent'` 的行。可见普通行的共享 ongoing loading 来自其已加载 parent 目录中正在运行的直接 child,绝不来自摘要谱系。Child 活动状态使用最新 UI status,尚无该状态时使用 Session 摘要。
|
|
182
|
+
|
|
183
|
+
行动画由 [AnimatedRows](src/client/rows/AnimatedRows.tsx) 负责。它仅在 React 提交改变行成员或顺序时读取更新前后的位置,并使用浏览器原生位移与透明度动画。被移除的行以不可交互的副本在滚动列表外淡出,不会延迟 React 卸载,也不会扩大列表的滚动范围。初始加载、拖拽提交、展开其余会话和视图选项变化直接完成。动画组件不使用布局观察器或轮询,也不会因仅内容更新或滚动而测量位置。
|
|
184
|
+
|
|
185
|
+
### 悬浮卡片
|
|
186
|
+
|
|
187
|
+
Workspace 与 Session 悬浮卡片会复制对应行被截断的值:激活 Workspace 卡片会写入其完整目录路径,激活非空白 Session 卡片则会写入其完整显示标题。临时的空白「新会话」卡片保持只读,因为其本地化标签是占位文案,并非会话内容。 Session 卡片在卡片打开期间还会在相对时间与行尾状态行之间渲染 `sidebar.session.row.hover` seat,与行自身状态无关。
|
|
188
|
+
|
|
189
|
+
</details>
|
|
190
|
+
|
|
191
|
+
-----
|
|
192
|
+
|
|
193
|
+
<a id="further-exploration"></a>
|
|
194
|
+
## 进一步探索
|
|
195
|
+
|
|
196
|
+
以下页面覆盖侧边栏宿主、主视觉区界面与选取后端。
|
|
197
|
+
|
|
198
|
+
- [ui-sidebar](../ui-sidebar/README.zh.md)——承载 `sidebar.workspaces` 子 slot 的侧边栏外壳。
|
|
199
|
+
- [ui-conversation](../ui-conversation/README.zh.md)——承载 Session Intent 主视觉区选择器子 slot 的聊天界面。
|
|
200
|
+
- [directory-picker-native](../../host/directory-picker-native/README.zh.md)——填充目录流子 slot 的 OS 选择器后端。
|
|
201
|
+
- [Workspace Controller](../../api/workspace-controller/README.zh.md)——负责 Workspace、成员关系与 Workspace 分组顺序的 Host 变更和框架无关 Client 投影。
|
|
202
|
+
|
|
203
|
+
-----
|
|
204
|
+
|
|
205
|
+
<a id="model-experience"></a>
|
|
206
|
+
## 模型体验
|
|
207
|
+
|
|
208
|
+
无。该包是浏览器端 UI 插件层,不注册任何面向模型的内容。
|
|
209
|
+
|
|
210
|
+
#### KV Cache 影响
|
|
211
|
+
|
|
212
|
+
无;该包既不组装也不发送提供方请求。
|
|
213
|
+
|
|
214
|
+
## 已知限制与延期工作
|
|
215
|
+
|
|
216
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
这些限制定义搜索深度、归档界面与选取载体;它们是当前包约束。
|
|
220
|
+
|
|
221
|
+
- **没有模糊内容搜索或事件深链接**:内容后端采用字面 token/短语匹配,选择结果会打开 Session,而不是匹配的事件。
|
|
222
|
+
- **没有 Session 删除**:会话可以归档但绝不会被删除;已归档的行通过「已归档会话」视图筛选与搜索结果中的取消归档操作原位恢复,删除 Workspace 注册记录不会删除 Session。
|
|
223
|
+
- **待处理的用户交互不会聚合到折叠的分组上**:折叠分组内正在等待的行不会点亮分组头指示,只有展开该分组后才可见。
|
|
224
|
+
- **原生文件夹选择依赖本地 Host 载体**:在 `-native` 组合下,进程内部署或远程浏览器部署无法打开本地操作系统对话框;可远程的选取是 `-browse` 组合的应用内流程。
|
|
225
|
+
|
|
226
|
+
<a id="dev-note"></a>
|
|
227
|
+
### 开发备注
|
|
228
|
+
|
|
229
|
+
<details>
|
|
230
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
231
|
+
|
|
232
|
+
无。
|
|
233
|
+
|
|
234
|
+
</details>
|