dsh-plugin-standalone-session 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.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The narrow, structurally typed DSH browser surface this plugin consumes.
3
+ *
4
+ * DSH's own client packages are provided by the running host's module graph,
5
+ * not by npm at this plugin's version. Declaring only the faces used here keeps
6
+ * the plugin buildable without importing stale published type packages.
7
+ */
8
+ import type { ComponentType } from 'react';
9
+ /** Observable snapshot source shared by the workspace and session controllers. */
10
+ export interface ObservableSnapshot<T> {
11
+ getSnapshot(): T;
12
+ subscribe(listener: () => void): () => void;
13
+ }
14
+ /** One Session catalog row, as consumed by the sidebar and this plugin's panel. */
15
+ export interface SessionRow {
16
+ readonly id: string;
17
+ readonly title?: string;
18
+ readonly displayTitle: string;
19
+ readonly cwd?: string;
20
+ readonly parentId?: string;
21
+ readonly origin?: 'subagent';
22
+ readonly running: boolean;
23
+ readonly blank: boolean;
24
+ readonly updatedAt: number;
25
+ }
26
+ /** Complete Session catalog snapshot. */
27
+ export interface SessionListState {
28
+ readonly ids: readonly string[];
29
+ readonly byId: Readonly<Record<string, SessionRow>>;
30
+ readonly phase?: string;
31
+ }
32
+ /** One workspace row from the Workspace follow stream. */
33
+ export interface WorkspaceRow {
34
+ readonly workspaceId: string;
35
+ readonly path: string;
36
+ readonly title: string;
37
+ readonly sessionIds: readonly string[];
38
+ }
39
+ /** Complete workspace snapshot. */
40
+ export interface WorkspaceSnapshot {
41
+ readonly items: readonly WorkspaceRow[];
42
+ readonly archivedSessionIds: readonly string[];
43
+ readonly pinnedSessionIds?: readonly string[];
44
+ readonly phase?: string;
45
+ }
46
+ /** Remote failure/result envelope returned by Session mutations. */
47
+ export type RemoteResult<T> = {
48
+ readonly ok: true;
49
+ readonly value: T;
50
+ } | {
51
+ readonly ok: false;
52
+ readonly error: {
53
+ readonly code: string;
54
+ readonly message: string;
55
+ };
56
+ };
57
+ /** One retained Client Session generation. */
58
+ export interface SessionReference {
59
+ readonly binding: {
60
+ readonly session: {
61
+ rename(title: string): Promise<RemoteResult<{
62
+ readonly title: string;
63
+ readonly seq: number;
64
+ }>>;
65
+ };
66
+ };
67
+ release(): void;
68
+ }
69
+ /** Browser Session service (`ctx.sessions`). */
70
+ export interface SessionService {
71
+ readonly list: ObservableSnapshot<SessionListState>;
72
+ create(options?: {
73
+ workspaceId?: string;
74
+ cwd?: string;
75
+ sessionId?: string;
76
+ agentPreset?: string;
77
+ }): Promise<string>;
78
+ using<T>(target: string, options: {
79
+ source: string;
80
+ signal?: AbortSignal;
81
+ }, operation: (reference: SessionReference) => T | Promise<T>): Promise<T>;
82
+ }
83
+ /** Browser Workspace service (`ctx.workspaces`). */
84
+ export interface WorkspaceService {
85
+ readonly list: ObservableSnapshot<WorkspaceSnapshot>;
86
+ create(input: {
87
+ path: string;
88
+ }): Promise<WorkspaceRow>;
89
+ delete(workspaceId: string): Promise<void>;
90
+ archiveSession(sessionId: string, options?: {
91
+ readonly stopActivity?: boolean;
92
+ }): Promise<void>;
93
+ unarchiveSession(sessionId: string): Promise<void>;
94
+ }
95
+ /** Cross-controller navigation (`ctx.uiWorkspace`). */
96
+ export interface UiWorkspaceService {
97
+ openSession(target: string): void;
98
+ startSession(workspaceId?: string): void;
99
+ }
100
+ /** Localized translate function. */
101
+ export interface Translate {
102
+ (key: string, values?: Readonly<Record<string, string | number>>): string;
103
+ }
104
+ /** Browser locale registry (`ctx.locale`). */
105
+ export interface LocaleService {
106
+ register(namespace: string, dictionaries: Readonly<Record<string, Readonly<Record<string, string>>>>): () => void;
107
+ bind(namespace: string): Translate;
108
+ }
109
+ /** One client command contribution accepted by `ctx.commandUi`. */
110
+ export interface CommandContribution {
111
+ readonly name: string;
112
+ label?(): string;
113
+ description?(): string;
114
+ readonly icon?: ComponentType<{
115
+ size?: number;
116
+ }>;
117
+ available(session: unknown): boolean;
118
+ readonly ui: {
119
+ readonly kind: 'action';
120
+ run(session: unknown): void;
121
+ } | {
122
+ readonly kind: 'popupSelect';
123
+ options(session: unknown, signal: AbortSignal): Promise<readonly unknown[]>;
124
+ onSelect(option: unknown, session: unknown): void | Promise<void>;
125
+ };
126
+ }
127
+ /** Browser command registry (`ctx.commandUi`). */
128
+ export interface CommandUiService {
129
+ register(contribution: CommandContribution): () => void;
130
+ }
131
+ /** Layout panel selection (`ctx.layout`). */
132
+ export interface LayoutService {
133
+ selectPanel(panelId: string | null): void;
134
+ }
135
+ /** One slot ledger entry (the registrant object behind a slot render). */
136
+ export interface SlotEntry {
137
+ readonly options?: {
138
+ readonly priority?: number;
139
+ readonly [key: string]: unknown;
140
+ };
141
+ component: ComponentType<any>;
142
+ readonly [key: string]: unknown;
143
+ }
144
+ /** Slot registry (`ctx.slots`). */
145
+ export interface SlotService {
146
+ inject(name: string, factory: () => unknown): () => void;
147
+ register(entry: Readonly<Record<string, unknown>>, component: ComponentType<any>): () => void;
148
+ entries(name: string): readonly SlotEntry[];
149
+ subscribe?(name: string, listener: () => void): () => void;
150
+ }
151
+ /** Cordis effect helper and service lookup. */
152
+ export interface ClientContext {
153
+ readonly slots: SlotService;
154
+ readonly locale: LocaleService;
155
+ readonly sessions: SessionService;
156
+ readonly workspaces: WorkspaceService;
157
+ readonly uiWorkspace: UiWorkspaceService;
158
+ readonly commandUi: CommandUiService;
159
+ readonly layout: LayoutService;
160
+ readonly remote?: {
161
+ readonly $host?: {
162
+ readonly home?: string;
163
+ };
164
+ };
165
+ effect(execute: () => (() => void) | void, label?: string): unknown;
166
+ get(name: string): unknown;
167
+ }
@@ -0,0 +1,10 @@
1
+ import type { Translate } from './dsh.js';
2
+ /**
3
+ * Localize `relativeTime` output. The shared helper stays locale-neutral for
4
+ * non-UI callers; this adapter maps its compact m/h/d units to copy.
5
+ * @param t - localized copy.
6
+ * @param timestamp - last update time.
7
+ * @param now - injectable clock for tests.
8
+ * @returns localized relative time, or an empty string for an invalid stamp.
9
+ */
10
+ export declare function relativeTimeLabel(t: Translate, timestamp: number, now?: number): string;
@@ -0,0 +1,31 @@
1
+ import type { ClientContext, ObservableSnapshot, SessionListState, SessionRow, WorkspaceSnapshot } from './dsh.js';
2
+ /** Subscribe one component to an observable snapshot. */
3
+ export declare function useSnapshot<T>(source: ObservableSnapshot<T>): T;
4
+ /** The complete Session catalog. */
5
+ export declare function useSessionList(ctx: ClientContext): SessionListState;
6
+ /** The complete Workspace snapshot (used for the archive set). */
7
+ export declare function useWorkspaceSnapshot(ctx: ClientContext): WorkspaceSnapshot;
8
+ /**
9
+ * One session's durable cwd. A missing row or id yields undefined, which is
10
+ * also the "not standalone" answer.
11
+ * @param ctx - client root context.
12
+ * @param sessionId - rendered session, if any.
13
+ * @returns the session cwd, or undefined.
14
+ */
15
+ export declare function useSessionCwd(ctx: ClientContext, sessionId: string | undefined): string | undefined;
16
+ /**
17
+ * Standalone rows in display order: blank sessions first, then newest activity.
18
+ * Archived rows are excluded; the native archived view remains the restore
19
+ * surface.
20
+ * @param sessions - catalog snapshot.
21
+ * @param archivedSessionIds - Host archive set.
22
+ * @returns the standalone rows.
23
+ */
24
+ export declare function standaloneRows(sessions: SessionListState, archivedSessionIds: readonly string[]): SessionRow[];
25
+ /**
26
+ * Count standalone rows without constructing the full display list.
27
+ * @param sessions - catalog snapshot.
28
+ * @param archivedSessionIds - Host archive set.
29
+ * @returns the number of live standalone sessions.
30
+ */
31
+ export declare function countStandalone(sessions: SessionListState, archivedSessionIds: readonly string[]): number;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Browser half of `dsh-plugin-standalone-session`: a dedicated sidebar/panel
3
+ * area, a direct creation action, a command contribution, native-row badges
4
+ * and the composer adapter that makes a workspace-free session usable.
5
+ *
6
+ * Composition is additive: every registration lands in an existing DSH list
7
+ * seat or keyed panel. The native New Session action, workspace browser,
8
+ * picker, session menus and chat surface are not replaced.
9
+ */
10
+ import type { ClientContext } from './dsh.js';
11
+ export { NEW_PANEL_ID, PANEL_ID } from './panels.js';
12
+ /** Services this client plugin requires. */
13
+ export declare const inject: string[];
14
+ /**
15
+ * Register the browser half.
16
+ * @param ctx - Cordis client root context.
17
+ */
18
+ export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,39 @@
1
+ import type { SessionListState } from './dsh.js';
2
+ /** How long a row click remains a valid launcher gesture. */
3
+ export declare const LAUNCHER_ARM_WINDOW_MS = 3000;
4
+ /** Record a real click on the launcher row. */
5
+ export declare function armStandaloneLauncher(now?: number): void;
6
+ /** Whether a recent real click armed the launcher, without consuming it. */
7
+ export declare function isStandaloneLauncherArmed(now?: number): boolean;
8
+ /** Consume the arm flag. */
9
+ export declare function clearStandaloneLauncherArm(): void;
10
+ /** Result of one launcher invocation. */
11
+ export interface StandaloneLaunchResult {
12
+ readonly sessionId: string;
13
+ readonly reused: boolean;
14
+ }
15
+ /** Injected facts and effects for {@link StandaloneLaunchCoordinator}. */
16
+ export interface StandaloneLaunchDeps {
17
+ readonly sessions: () => SessionListState;
18
+ readonly archivedSessionIds: () => readonly string[];
19
+ readonly create: () => Promise<string>;
20
+ }
21
+ /**
22
+ * Serialize launcher creates and reuse the last blank session it created.
23
+ * The class never opens a session; the caller owns navigation and its own
24
+ * mounted guard.
25
+ */
26
+ export declare class StandaloneLaunchCoordinator {
27
+ private readonly deps;
28
+ private lastLauncherSessionId;
29
+ private inFlight;
30
+ constructor(deps: StandaloneLaunchDeps);
31
+ /**
32
+ * Launch one conversation: reuse the last blank bridged session when it is
33
+ * still live, otherwise create a new one.
34
+ * @returns the session to open and whether it was reused.
35
+ */
36
+ launch(): Promise<StandaloneLaunchResult>;
37
+ private run;
38
+ private reusableSession;
39
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Browser dictionaries. `zh` is the key-set source of truth; the checked type
3
+ * keeps `en` complete, and both shipped languages are registered together.
4
+ */
5
+ export declare const NS = "standaloneSession";
6
+ export type StandaloneKey = 'panel.label' | 'panel.title' | 'panel.description' | 'panel.new' | 'panel.empty' | 'panel.emptyHint' | 'panel.badge' | 'panel.open' | 'panel.rename' | 'panel.delete' | 'panel.untitled' | 'panel.blank' | 'panel.running' | 'panel.cwd' | 'panel.count' | 'panel.deleteTitle' | 'panel.deleteBody' | 'panel.deleteConfirm' | 'panel.deleteKeepDir' | 'panel.cancel' | 'panel.renameTitle' | 'panel.renameLabel' | 'panel.more' | 'panel.save' | 'panel.titleRequired' | 'panel.actionFailed' | 'panel.retry' | 'panel.archivedHint' | 'panel.loading' | 'entry.label' | 'launcher.creating' | 'launcher.failedTitle' | 'launcher.retry' | 'launcher.manage' | 'launcher.hint' | 'command.label' | 'command.description' | 'command.manageLabel' | 'command.manageDescription' | 'footer.manage' | 'footer.manageCount' | 'footer.manageTooltip' | 'time.now' | 'time.minutes' | 'time.hours' | 'time.days' | 'toast.manageFailed' | 'hero.badge' | 'hero.hint' | 'row.mark' | 'row.hover' | 'composer.placeholder' | 'toast.created' | 'toast.createFailed' | 'toast.deleteFailed' | 'toast.deleted' | 'toast.renamed' | 'toast.renameFailed' | 'error.noHome' | 'error.noSession';
7
+ export type StandaloneDict = Readonly<Record<StandaloneKey, string>>;
8
+ export declare const zh: StandaloneDict;
9
+ export declare const en: StandaloneDict;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A tiny process-local notice store backing one `shell.overlay` toast. It is
3
+ * deliberately session-free: creation errors can happen on the New Session
4
+ * page, where no session exists to host a native notice.
5
+ */
6
+ export type NoticeKind = 'info' | 'error';
7
+ export interface Notice {
8
+ readonly id: number;
9
+ readonly kind: NoticeKind;
10
+ readonly text: string;
11
+ }
12
+ /** Read the current notice (stable reference between changes). */
13
+ export declare function getNoticeSnapshot(): Notice | null;
14
+ /** Subscribe to notice changes. */
15
+ export declare function subscribeNotice(listener: () => void): () => void;
16
+ /**
17
+ * Show or replace the toast.
18
+ * @param kind - success/informational or failure styling.
19
+ * @param text - already-localized copy.
20
+ */
21
+ export declare function showNotice(kind: NoticeKind, text: string): void;
22
+ /** Dismiss the toast immediately. */
23
+ export declare function dismissNotice(): void;
@@ -0,0 +1,3 @@
1
+ /** Stable main-panel identities shared by the launcher, manager and commands. */
2
+ export declare const PANEL_ID = "standalone-sessions";
3
+ export declare const NEW_PANEL_ID = "standalone-sessions-new";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * One namespaced stylesheet. DSH theme variables carry the palette, so the
3
+ * plugin follows light/dark themes without owning color tokens.
4
+ */
5
+ export declare const CSS = "\n.ss-panel {\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n overflow: auto;\n padding: 32px clamp(20px, 5vw, 56px) 56px;\n color: var(--dsw-alias-label-primary, #1f2328);\n background: var(--dsw-specific-main-fill, transparent);\n}\n.ss-shell {\n width: 100%;\n max-width: 880px;\n margin: 0 auto;\n}\n.ss-header {\n display: flex;\n align-items: flex-start;\n gap: 16px;\n justify-content: space-between;\n flex-wrap: wrap;\n margin-bottom: 8px;\n}\n.ss-titleRow {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n}\n.ss-title {\n font-size: 24px;\n line-height: 32px;\n font-weight: 600;\n margin: 0;\n}\n.ss-description {\n color: var(--dsw-alias-label-secondary, #59636e);\n font-size: 13px;\n line-height: 20px;\n margin: 6px 0 0;\n max-width: 560px;\n}\n.ss-count {\n color: var(--dsw-alias-label-tertiary, #818b98);\n font-size: 12px;\n line-height: 20px;\n margin-top: 6px;\n}\n.ss-countInline {\n margin-top: 0;\n align-self: center;\n}\n.ss-button {\n font: inherit;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n box-sizing: border-box;\n min-height: 32px;\n padding: 5px 12px;\n border-radius: var(--dsw-radius-md, 10px);\n border: 0.5px solid transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-primary, #1f2328);\n background: var(--dsw-alias-button-elevated-fill, #f3f4f6);\n transition: background 120ms ease, border-color 120ms ease;\n}\n.ss-button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, #e9ebee);\n}\n.ss-button:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid\n var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary, #3b82f6));\n outline-offset: 1px;\n}\n.ss-button:disabled {\n cursor: default;\n opacity: 0.55;\n}\n.ss-buttonPrimary {\n color: #fff;\n background: var(--dsw-alias-state-business-primary, #2563eb);\n border-color: var(--dsw-alias-state-business-primary, #2563eb);\n}\n.ss-buttonPrimary:hover:not(:disabled) {\n background: var(--dsw-alias-state-business-primary, #2563eb);\n filter: brightness(1.05);\n}\n.ss-buttonQuiet {\n background: transparent;\n border-color: var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.22));\n}\n.ss-buttonDanger {\n color: var(--dsw-alias-state-error-primary, #d1242f);\n background: transparent;\n border-color: var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.22));\n}\n.ss-badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n flex: none;\n border-radius: 999px;\n padding: 1px 8px 2px;\n font-size: 11px;\n line-height: 16px;\n font-weight: 600;\n letter-spacing: 0.01em;\n color: var(--dsw-alias-label-primary-bluish, #1f4f8f);\n background: var(--dsw-alias-state-business-tertiary, rgba(59, 130, 246, 0.12));\n border: 0.5px solid var(--dsw-alias-interactive-bg-hover, rgba(59, 130, 246, 0.18));\n}\n.ss-list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin: 20px 0 0;\n padding: 0;\n list-style: none;\n}\n.ss-row {\n position: relative;\n display: flex;\n align-items: center;\n gap: 14px;\n box-sizing: border-box;\n padding: 12px 14px;\n border-radius: var(--dsw-radius-lg, 14px);\n border: 0.5px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.2));\n background: var(--dsw-alias-bg-elevated, rgba(255, 255, 255, 0.02));\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n.ss-row:hover,\n.ss-row:focus-within {\n border-color: var(--dsw-alias-border-l4, rgba(128, 128, 128, 0.35));\n background: var(--dsw-alias-interactive-bg-hover, #e9ebee);\n}\n.ss-row:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid\n var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary, #3b82f6));\n outline-offset: 1px;\n}\n.ss-row[aria-disabled=\"true\"] {\n cursor: default;\n opacity: 0.7;\n}\n.ss-rowMain {\n min-width: 0;\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n.ss-rowTitleRow {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n.ss-rowTitle {\n font-size: 14px;\n line-height: 20px;\n font-weight: 600;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ss-rowMeta {\n color: var(--dsw-alias-label-tertiary, #818b98);\n font-size: 12px;\n line-height: 18px;\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n flex-wrap: wrap;\n}\n.ss-dotSep {\n opacity: 0.5;\n}\n.ss-rowActions {\n position: relative;\n display: flex;\n align-items: center;\n gap: 6px;\n flex: none;\n}\n.ss-rowMenuButton {\n width: 28px;\n height: 28px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 0;\n border-radius: var(--dsw-radius-md, 10px);\n background: transparent;\n color: var(--dsw-alias-label-tertiary, #818b98);\n font: inherit;\n font-size: 18px;\n line-height: 1;\n cursor: pointer;\n opacity: 0;\n transition: background 120ms ease, color 120ms ease, opacity 120ms ease;\n}\n.ss-row:hover .ss-rowMenuButton,\n.ss-row:focus-within .ss-rowMenuButton,\n.ss-rowMenuOpen .ss-rowMenuButton {\n opacity: 1;\n}\n.ss-rowMenuButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary, #1f2328);\n background: var(--dsw-alias-interactive-bg-hover, #e9ebee);\n}\n.ss-rowMenuButton:focus-visible {\n opacity: 1;\n outline: var(--dsw-focus-ring-width, 2px) solid\n var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary, #3b82f6));\n outline-offset: 1px;\n}\n.ss-rowMenuButton:disabled {\n cursor: default;\n opacity: 0.35;\n}\n.ss-rowMenu {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n z-index: 20;\n min-width: 136px;\n padding: 4px;\n display: flex;\n flex-direction: column;\n gap: 2px;\n border: 0.5px solid var(--dsw-alias-border-l4, rgba(128, 128, 128, 0.35));\n border-radius: var(--dsw-radius-md, 10px);\n background: var(--dsw-alias-bg-elevated, #fff);\n box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);\n}\n.ss-rowMenu button {\n box-sizing: border-box;\n width: 100%;\n padding: 6px 9px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-primary, #1f2328);\n font: inherit;\n font-size: 13px;\n line-height: 20px;\n text-align: left;\n cursor: pointer;\n}\n.ss-rowMenu button:hover:not(:disabled),\n.ss-rowMenu button:focus-visible {\n background: var(--dsw-alias-interactive-bg-hover, #e9ebee);\n outline: none;\n}\n.ss-rowMenu button:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.ss-rowMenuDanger {\n color: var(--dsw-alias-state-error-primary, #d1242f) !important;\n}\n.ss-status {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n}\n.ss-statusDot {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: currentColor;\n opacity: 0.55;\n}\n.ss-stateRunning {\n color: var(--dsw-alias-state-business-primary, #2563eb);\n font-weight: 600;\n}\n.ss-empty {\n margin-top: 28px;\n padding: 44px 24px;\n text-align: center;\n border: 0.5px dashed var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.3));\n border-radius: var(--dsw-radius-lg, 14px);\n}\n.ss-emptyTitle {\n font-size: 15px;\n font-weight: 600;\n line-height: 22px;\n margin: 0 0 6px;\n}\n.ss-emptyHint {\n color: var(--dsw-alias-label-secondary, #59636e);\n font-size: 13px;\n line-height: 20px;\n margin: 0 auto;\n max-width: 460px;\n}\n.ss-emptyAction {\n margin-top: 18px;\n}\n.ss-panelHint {\n margin: 18px 0 0;\n color: var(--dsw-alias-label-tertiary, #818b98);\n font-size: 12px;\n line-height: 20px;\n}\n.ss-fieldLabel {\n display: block;\n margin: 12px 0 6px;\n color: var(--dsw-alias-label-secondary, #59636e);\n font-size: 12px;\n line-height: 18px;\n}\n.ss-error {\n margin-top: 16px;\n padding: 8px 10px;\n border-radius: var(--dsw-radius-sm, 8px);\n color: var(--dsw-alias-state-error-primary, #d1242f);\n background: var(--dsw-alias-state-error-tertiary, rgba(209, 36, 47, 0.08));\n font-size: 12px;\n line-height: 18px;\n}\n.ss-dialogBackdrop {\n position: fixed;\n inset: 0;\n z-index: 80;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.28);\n padding: 24px;\n}\n.ss-dialog {\n width: 100%;\n max-width: 440px;\n box-sizing: border-box;\n border-radius: var(--dsw-radius-lg, 14px);\n border: 0.5px solid var(--dsw-alias-border-l4, rgba(128, 128, 128, 0.35));\n background: var(--dsw-alias-bg-elevated, #fff);\n padding: 20px;\n box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22);\n}\n.ss-dialogTitle {\n margin: 0 0 6px;\n font-size: 16px;\n line-height: 24px;\n font-weight: 600;\n}\n.ss-dialogBody {\n margin: 0 0 14px;\n color: var(--dsw-alias-label-secondary, #59636e);\n font-size: 13px;\n line-height: 20px;\n}\n.ss-input {\n box-sizing: border-box;\n width: 100%;\n height: 36px;\n border-radius: var(--dsw-radius-sm, 8px);\n border: 0.5px solid var(--dsw-alias-border-l4, rgba(128, 128, 128, 0.35));\n background: transparent;\n color: inherit;\n font: inherit;\n padding: 0 10px;\n}\n.ss-dialogFooter {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 18px;\n}\n.ss-footerAction {\n box-sizing: border-box;\n width: 100%;\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: 30px;\n padding: 4px 8px;\n border: 0;\n border-radius: var(--dsw-radius-md, 10px);\n background: transparent;\n color: var(--dsw-alias-label-secondary, #59636e);\n font: inherit;\n cursor: pointer;\n text-align: left;\n}\n.ss-footerAction:hover {\n color: var(--dsw-alias-label-primary, #1f2328);\n background: var(--dsw-alias-interactive-bg-hover, #e9ebee);\n}\n.ss-footerAction:disabled {\n cursor: default;\n opacity: 0.55;\n}\n.ss-footerLabel {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n}\n.ss-footerRail {\n width: 36px;\n height: 36px;\n justify-content: center;\n padding: 0;\n}\n.ss-panelIcon {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n.ss-launcher {\n box-sizing: border-box;\n min-height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 32px;\n color: var(--dsw-alias-label-primary, #1f2328);\n}\n.ss-launcherCard {\n box-sizing: border-box;\n width: 100%;\n max-width: 430px;\n padding: 30px 28px;\n text-align: center;\n border: 0.5px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.22));\n border-radius: var(--dsw-radius-lg, 14px);\n background: var(--dsw-alias-bg-elevated, rgba(255, 255, 255, 0.02));\n}\n.ss-launcherSpinner {\n width: 22px;\n height: 22px;\n margin: 0 auto 14px;\n border-radius: 50%;\n border: 2px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));\n border-top-color: var(--dsw-alias-state-business-primary, #2563eb);\n animation: ss-spin 900ms linear infinite;\n}\n@keyframes ss-spin {\n to { transform: rotate(360deg); }\n}\n.ss-launcherTitle {\n margin: 0;\n font-size: 15px;\n line-height: 22px;\n font-weight: 600;\n}\n.ss-launcherHint {\n margin: 6px 0 0;\n color: var(--dsw-alias-label-secondary, #59636e);\n font-size: 13px;\n line-height: 20px;\n}\n.ss-launcherActions {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 8px;\n margin-top: 18px;\n}\n.ss-heroBadge {\n margin-right: 4px;\n}\n.ss-rowMark {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n border-radius: 4px;\n font-size: 10px;\n line-height: 1;\n font-weight: 700;\n color: var(--dsw-alias-label-primary-bluish, #1f4f8f);\n background: var(--dsw-alias-state-business-tertiary, rgba(59, 130, 246, 0.14));\n flex: none;\n}\n.ss-rowHover {\n display: flex;\n align-items: center;\n gap: 6px;\n color: #cfd3d6;\n font-size: 12px;\n line-height: 16px;\n}\n.ss-toast {\n position: fixed;\n left: 50%;\n bottom: 28px;\n transform: translateX(-50%);\n z-index: 90;\n max-width: min(520px, calc(100vw - 40px));\n box-sizing: border-box;\n padding: 9px 14px;\n border-radius: var(--dsw-radius-md, 10px);\n border: 0.5px solid var(--dsw-alias-border-l4, rgba(128, 128, 128, 0.35));\n background: var(--dsw-alias-bg-elevated, #fff);\n color: var(--dsw-alias-label-primary, #1f2328);\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);\n font-size: 13px;\n line-height: 19px;\n pointer-events: auto;\n}\n.ss-toastError {\n color: var(--dsw-alias-state-error-primary, #d1242f);\n}\n.ss-visuallyHidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n@media (hover: none) {\n .ss-rowMenuButton { opacity: 1; }\n}\n@media (max-width: 640px) {\n .ss-panel { padding: 20px 14px 40px; }\n .ss-row { flex-wrap: wrap; }\n .ss-rowActions { width: 100%; justify-content: flex-end; }\n}\n";
6
+ /**
7
+ * Insert (or replace) the plugin's one style element.
8
+ * @returns a disposer that removes the element.
9
+ */
10
+ export declare function insertStyles(): () => void;
@@ -0,0 +1,124 @@
1
+ import { type StandaloneEntry, type StandaloneRegistryData } from '../shared/standalone.js';
2
+ /** Minimal logger surface; DSH's logger and tests both satisfy it. */
3
+ export interface RegistryLogger {
4
+ info(message: string): void;
5
+ warn(message: string): void;
6
+ }
7
+ /** Default logger: console with the plugin's uniform prefix. */
8
+ export declare function prefixedLogger(prefix?: string): RegistryLogger;
9
+ /** Outcome of one session-directory cleanup. */
10
+ export type CleanupOutcome = {
11
+ readonly kind: 'removed';
12
+ } | {
13
+ readonly kind: 'retained';
14
+ readonly reason: string;
15
+ } | {
16
+ readonly kind: 'failed';
17
+ readonly error: string;
18
+ };
19
+ /** Facts used by {@link cleanupSessionDirectory}; injectable for tests. */
20
+ export interface DirectoryOps {
21
+ stat(path: string): Promise<{
22
+ isDirectory(): boolean;
23
+ }>;
24
+ rm(path: string, options: {
25
+ recursive: true;
26
+ force: true;
27
+ }): Promise<void>;
28
+ }
29
+ /**
30
+ * Whether one path is inside another (same path counts).
31
+ * @param parent - containing directory.
32
+ * @param child - candidate path.
33
+ * @returns true when `child` is at or below `parent` lexically.
34
+ */
35
+ export declare function isPathInside(parent: string, child: string): boolean;
36
+ /**
37
+ * Whether the record's cwd is structurally one of this plugin's session
38
+ * directories: an absolute path whose final segment is the session id and
39
+ * whose parent is the standalone marker directory. This remains true when a
40
+ * custom `DSH_HOME` puts the root somewhere other than this process's root.
41
+ * @param entry - registry record.
42
+ * @returns whether deletion may target that directory.
43
+ */
44
+ export declare function isOwnedSessionDirectory(entry: StandaloneEntry): boolean;
45
+ /**
46
+ * Remove one session directory according to its record. Never deletes a path
47
+ * that is not the record's own session directory.
48
+ * @param entry - registry record.
49
+ * @param ops - filesystem operations, injectable for tests.
50
+ * @param logger - diagnostic sink.
51
+ * @returns the cleanup outcome.
52
+ */
53
+ export declare function cleanupSessionDirectory(entry: StandaloneEntry, ops?: DirectoryOps, logger?: RegistryLogger): Promise<CleanupOutcome>;
54
+ /**
55
+ * Durable registry with serialized, atomic writes.
56
+ *
57
+ * Every mutation runs on one promise chain, so concurrent session
58
+ * announcements cannot lose each other's records. A failed write keeps the
59
+ * in-memory value and surfaces the error to the caller; the next write retries
60
+ * the complete document.
61
+ */
62
+ export declare class DurableStandaloneRegistry {
63
+ readonly rootDir: string;
64
+ readonly filePath: string;
65
+ private data;
66
+ private queue;
67
+ private loadPromise;
68
+ private loaded;
69
+ private readonly logger;
70
+ private readonly ops;
71
+ /**
72
+ * @param rootDir - directory holding session directories and the registry file.
73
+ * @param logger - diagnostic sink.
74
+ * @param ops - directory operations, injectable for tests.
75
+ */
76
+ constructor(rootDir: string, logger?: RegistryLogger, ops?: DirectoryOps);
77
+ /** Load the registry once, recovering a corrupt file by moving it aside. */
78
+ load(): Promise<void>;
79
+ private loadOnce;
80
+ /** Current immutable snapshot. */
81
+ snapshot(): StandaloneRegistryData;
82
+ /** All records in stable id order. */
83
+ list(): StandaloneEntry[];
84
+ /** One record, if present. */
85
+ get(sessionId: string): StandaloneEntry | undefined;
86
+ /** Await every queued mutation. */
87
+ flush(): Promise<void>;
88
+ /**
89
+ * Announce one standalone session (idempotent).
90
+ * @param summary - Host session summary facts.
91
+ */
92
+ announce(summary: {
93
+ readonly sessionId: string;
94
+ readonly cwd?: string;
95
+ readonly blank?: boolean;
96
+ }): Promise<void>;
97
+ /**
98
+ * Mark one session as having carried a user-authored durable message.
99
+ * @param sessionId - session identity.
100
+ */
101
+ markActivity(sessionId: string): Promise<void>;
102
+ /**
103
+ * Mark archived sessions and clean the ones whose archive is visible.
104
+ * @param archivedSessionIds - the Host's complete archive set.
105
+ * @returns cleanup outcomes keyed by session.
106
+ */
107
+ reconcileArchived(archivedSessionIds: ReadonlySet<string>, stillArchived?: (sessionId: string) => boolean): Promise<ReadonlyMap<string, CleanupOutcome>>;
108
+ /**
109
+ * Remove old empty session directories that neither the registry nor the
110
+ * live session store references. These are the residue of a failed create or
111
+ * a crash between mkdir and the session announcement.
112
+ * @param liveSessionIds - identities the Host currently holds.
113
+ * @param minimumAgeMs - only directories older than this are eligible.
114
+ * @returns identities whose directory was removed.
115
+ */
116
+ sweepOrphanDirectories(liveSessionIds: ReadonlySet<string>, minimumAgeMs?: number): Promise<string[]>;
117
+ /**
118
+ * Apply one pure mutation and persist it when the reference changed.
119
+ * @param mutator - pure registry transform.
120
+ */
121
+ private mutate;
122
+ /** Atomically replace the registry file. */
123
+ private write;
124
+ }
@@ -0,0 +1,37 @@
1
+ import { type RegistryLogger } from './host/registry.js';
2
+ /** Cordis function-plugin name. */
3
+ export declare const name = "standalone-session";
4
+ /** The tidy shape of the events this plugin listens to. */
5
+ interface SessionAddedSummary {
6
+ readonly sessionId?: string;
7
+ readonly cwd?: string;
8
+ readonly blank?: boolean;
9
+ }
10
+ /** The narrow Host context surface this plugin uses. */
11
+ export interface StandaloneHostContext {
12
+ on(event: 'api-session/added', listener: (summary: SessionAddedSummary) => void): unknown;
13
+ on(event: 'api-session/activity', listener: (sessionId: string) => void): unknown;
14
+ effect(execute: () => (() => void) | void, label?: string): unknown;
15
+ get(name: string): unknown;
16
+ root?: {
17
+ get?: (name: string) => unknown;
18
+ };
19
+ logger?: Partial<RegistryLogger>;
20
+ }
21
+ /**
22
+ * Resolve the standalone root from the OS home.
23
+ *
24
+ * The browser half only receives `$host.home`, so both halves must agree on
25
+ * this path. DSH's default home is `~/.dsh`; a custom `DSH_HOME` moves DSH's
26
+ * own profile data but not this plugin's dedicated directory. Keeping the two
27
+ * halves in agreement is more important than following a custom harness home.
28
+ * @param home - OS home, injectable for tests.
29
+ * @returns `<home>/.dsh/dsh-standalone-sessions`.
30
+ */
31
+ export declare function resolveStandaloneRoot(home?: string): string;
32
+ /**
33
+ * Register the host half.
34
+ * @param ctx - Cordis Host context.
35
+ */
36
+ export declare function apply(ctx: StandaloneHostContext): void;
37
+ export {};