@linxin666/dsh-client-ui-task-board 0.3.9 → 0.3.11
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/lib/client.js +56 -11
- package/lib/client.js.map +1 -1
- package/lib/index.js +20 -1
- package/lib/types/client/board-mount.d.ts +4 -1
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/sidebar-entry-core.d.ts +10 -0
- package/lib/types/client/sidebar-entry.d.ts +7 -1
- package/lib/types/host/run-guarded.d.ts +39 -0
- package/lib/types/host-runner.d.ts +1 -0
- package/package.json +19 -19
- package/src/client/board-mount.tsx +11 -1
- package/src/client/board.module.css +4 -2
- package/src/client/index.ts +10 -4
- package/src/client/locales.ts +15 -0
- package/src/client/sidebar-entry-core.ts +33 -7
- package/src/client/sidebar-entry.ts +7 -1
- package/src/core/tasks.ts +2 -1
- package/src/host/run-guarded.ts +76 -0
- package/src/host-runner.ts +20 -0
package/lib/index.js
CHANGED
|
@@ -369,7 +369,7 @@ function settleExecution(task, executionId, outcome, now, error) {
|
|
|
369
369
|
};
|
|
370
370
|
const executions = [...task.executions];
|
|
371
371
|
executions[index] = settled;
|
|
372
|
-
const status = outcome === "succeeded" ? "done" : outcome === "failed" ? "failed" : task.status === "running" ? "todo" : task.status;
|
|
372
|
+
const status = outcome === "succeeded" ? task.schedule?.enabled ? "todo" : "done" : outcome === "failed" ? "failed" : task.status === "running" ? "todo" : task.status;
|
|
373
373
|
return {
|
|
374
374
|
...task,
|
|
375
375
|
status,
|
|
@@ -2013,6 +2013,10 @@ function isServiceUnavailable(error) {
|
|
|
2013
2013
|
const code = error.code;
|
|
2014
2014
|
return code === "service-unavailable" || code === "gateway/service-unavailable";
|
|
2015
2015
|
}
|
|
2016
|
+
function isInvocationUnavailable(error) {
|
|
2017
|
+
const code = error.code;
|
|
2018
|
+
return code === "invocation-unavailable" || code === "gateway/invocation-unavailable";
|
|
2019
|
+
}
|
|
2016
2020
|
const SERVICE_UNAVAILABLE_ATTEMPTS = 5;
|
|
2017
2021
|
const SERVICE_UNAVAILABLE_BACKOFF_MS = 2e3;
|
|
2018
2022
|
function delay(ms) {
|
|
@@ -2093,6 +2097,7 @@ var HostExecutionRunner = class {
|
|
|
2093
2097
|
scanMemos = /* @__PURE__ */ new Map();
|
|
2094
2098
|
unavailableAttempts;
|
|
2095
2099
|
unavailableBackoffMs;
|
|
2100
|
+
unsupportedSessionListWarned = false;
|
|
2096
2101
|
constructor(gateway, commands, workspaceRegistry, unavailableRetry) {
|
|
2097
2102
|
this.gateway = gateway;
|
|
2098
2103
|
this.commands = commands;
|
|
@@ -2167,6 +2172,13 @@ var HostExecutionRunner = class {
|
|
|
2167
2172
|
items: response.items
|
|
2168
2173
|
};
|
|
2169
2174
|
} catch (error) {
|
|
2175
|
+
if (isInvocationUnavailable(error)) {
|
|
2176
|
+
if (!this.unsupportedSessionListWarned) {
|
|
2177
|
+
this.unsupportedSessionListWarned = true;
|
|
2178
|
+
console.warn("[dsh-task-board] DSH runtime session endpoint unavailable (requires DSH >= 0.1.2-alpha.2); task board roster auto-discovery is disabled", error);
|
|
2179
|
+
}
|
|
2180
|
+
return { known: false };
|
|
2181
|
+
}
|
|
2170
2182
|
if (!isServiceUnavailable(error) || attempt >= this.unavailableAttempts) {
|
|
2171
2183
|
console.error("[dsh-task-board] session/list failed; treating the host session roster as unknown", error);
|
|
2172
2184
|
return { known: false };
|
|
@@ -2183,6 +2195,13 @@ var HostExecutionRunner = class {
|
|
|
2183
2195
|
try {
|
|
2184
2196
|
response = await this.invoke("session", "list", {});
|
|
2185
2197
|
} catch (error) {
|
|
2198
|
+
if (isInvocationUnavailable(error)) {
|
|
2199
|
+
if (!this.unsupportedSessionListWarned) {
|
|
2200
|
+
this.unsupportedSessionListWarned = true;
|
|
2201
|
+
console.warn("[dsh-task-board] DSH runtime session endpoint unavailable (requires DSH >= 0.1.2-alpha.2); task board roster auto-discovery is disabled", error);
|
|
2202
|
+
}
|
|
2203
|
+
return { outcome: "pending" };
|
|
2204
|
+
}
|
|
2186
2205
|
console.warn("[dsh-task-board] session/list failed during execution inspection; keeping the outcome pending", error);
|
|
2187
2206
|
return { outcome: "pending" };
|
|
2188
2207
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { BoardController } from '../core/controller.ts';
|
|
2
|
+
import type { LocaleRefreshSource } from './sidebar-entry.ts';
|
|
2
3
|
/** The injected board container (kept in the DOM, hidden when inactive). */
|
|
3
4
|
export declare const BOARD_VIEW_SELECTOR = "[data-dsh-taskboard-view]";
|
|
4
5
|
/**
|
|
5
6
|
* Mount the board React tree into the center column and bind its visibility
|
|
6
7
|
* to the controller's boardOpen state.
|
|
7
8
|
* @param controller - the board controller driving the view.
|
|
9
|
+
* @param locale - locale-change source; when given, re-renders a mounted board
|
|
10
|
+
* on a Language switch.
|
|
8
11
|
* @returns disposer unmounting the tree and restoring the column.
|
|
9
12
|
*/
|
|
10
|
-
export declare function mountBoard(controller: BoardController): () => void;
|
|
13
|
+
export declare function mountBoard(controller: BoardController, locale?: LocaleRefreshSource): () => void;
|
|
@@ -155,5 +155,7 @@ export type TaskBoardKey = keyof typeof zh;
|
|
|
155
155
|
export type SettingsCardKey = TaskBoardKey;
|
|
156
156
|
/** Active dictionary, picked by the document language at call time. */
|
|
157
157
|
export declare function dictionary(): Record<TaskBoardKey, string>;
|
|
158
|
+
/** Wire the SDK translate seat; pass undefined to restore the document-language pick. */
|
|
159
|
+
export declare function setRuntimeTranslate(t: ((key: TaskBoardKey, params?: Record<string, string>) => string) | undefined): void;
|
|
158
160
|
/** Translate a key with optional {name} template params. */
|
|
159
161
|
export declare function t(key: TaskBoardKey, params?: Record<string, string>): string;
|
|
@@ -34,6 +34,16 @@ export interface SidebarEntryOptions {
|
|
|
34
34
|
label(): string;
|
|
35
35
|
/** Optional localized tooltip (title attribute). */
|
|
36
36
|
tooltip?(): string;
|
|
37
|
+
/**
|
|
38
|
+
* Optional locale-change subscription: re-applies label / aria-label /
|
|
39
|
+
* tooltip whenever the active locale changes. Plain-DOM rows would
|
|
40
|
+
* otherwise keep the label captured at mount; pass the SDK locale runtime
|
|
41
|
+
* subscription (ctx.locale.subscribe) so the row follows the language
|
|
42
|
+
* switch without a reload.
|
|
43
|
+
*/
|
|
44
|
+
refresh?: {
|
|
45
|
+
subscribe(listener: () => void): () => void;
|
|
46
|
+
};
|
|
37
47
|
/** Click action (open/toggle the owning panel). */
|
|
38
48
|
onToggle(): void;
|
|
39
49
|
/** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
|
|
@@ -14,10 +14,16 @@
|
|
|
14
14
|
import type { BoardController } from '../core/controller.ts';
|
|
15
15
|
/** Stable data attribute identifying the injected entry row. */
|
|
16
16
|
export declare const ENTRY_SELECTOR = "[data-dsh-taskboard-entry]";
|
|
17
|
+
/** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
|
|
18
|
+
export interface LocaleRefreshSource {
|
|
19
|
+
subscribe(listener: () => void): () => void;
|
|
20
|
+
}
|
|
17
21
|
/**
|
|
18
22
|
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
19
23
|
* on later React re-renders.
|
|
20
24
|
* @param controller - the board controller the entry toggles.
|
|
25
|
+
* @param locale - locale-change source; when given, re-applies the label on
|
|
26
|
+
* a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
|
|
21
27
|
* @returns disposer removing the entry and its observers.
|
|
22
28
|
*/
|
|
23
|
-
export declare function mountSidebarEntry(controller: BoardController): () => void;
|
|
29
|
+
export declare function mountSidebarEntry(controller: BoardController, locale?: LocaleRefreshSource): () => void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async-boundary guard shared by the plugin family's Host halves: every
|
|
3
|
+
* fire-and-forget promise chain and callback the host runtime does not own
|
|
4
|
+
* (route handlers, timers, event listeners, spawned work) funnels through
|
|
5
|
+
* these helpers. The dsh host installs a process-level fail-loud guard that
|
|
6
|
+
* turns ANY unhandled promise rejection into a whole-process exit — one
|
|
7
|
+
* plugin's stray rejection would otherwise take every plugin down. These
|
|
8
|
+
* helpers exist so that failure mode is structurally impossible in family
|
|
9
|
+
* code: the rejection becomes a logged error at the plugin boundary instead.
|
|
10
|
+
*
|
|
11
|
+
* Complements the aggregate's shell isolation (packages/dsh-web-all): the
|
|
12
|
+
* shell contains import/activation failures at boot; runGuarded contains
|
|
13
|
+
* run-time failures after activation.
|
|
14
|
+
* @module dsh-web-shared/host/run-guarded
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Run one async operation, logging (never propagating) a rejection. Use for
|
|
18
|
+
* promises whose lifecycle the caller does not await: spawned work, retry
|
|
19
|
+
* loops, background flushes. Returns the original promise so callers can
|
|
20
|
+
* still chain when they want to.
|
|
21
|
+
* @param promise - the work to shield; any thenable.
|
|
22
|
+
* @param label - log prefix naming the work site.
|
|
23
|
+
* @param log - error sink; defaults to console.error.
|
|
24
|
+
* @returns the input promise (rejection already consumed).
|
|
25
|
+
*/
|
|
26
|
+
export declare function runGuarded<T>(promise: PromiseLike<T>, label: string, log?: (error: unknown) => void): PromiseLike<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Wrap one callback so every invocation is individually guarded: a rejection
|
|
29
|
+
* inside one call is logged and swallowed instead of escaping into whatever
|
|
30
|
+
* infrastructure invoked the callback (HTTP server, EventEmitter, interval).
|
|
31
|
+
* Sync throws are caught identically; a returned promise is replaced by
|
|
32
|
+
* `undefined` after guarding (callers that need the original rejection should
|
|
33
|
+
* await inside their own try/catch instead).
|
|
34
|
+
* @param label - log prefix naming the callback site.
|
|
35
|
+
* @param handler - the work to guard.
|
|
36
|
+
* @param log - error sink; defaults to console.error.
|
|
37
|
+
* @returns a wrapped callback with the same parameter list.
|
|
38
|
+
*/
|
|
39
|
+
export declare function guardedHandler<A extends unknown[]>(label: string, handler: (...args: A) => unknown, log?: (error: unknown) => void): (...args: A) => unknown;
|
|
@@ -58,6 +58,7 @@ export declare class HostExecutionRunner {
|
|
|
58
58
|
private readonly scanMemos;
|
|
59
59
|
private readonly unavailableAttempts;
|
|
60
60
|
private readonly unavailableBackoffMs;
|
|
61
|
+
private unsupportedSessionListWarned;
|
|
61
62
|
constructor(gateway: SessionGateway | TypertGateway, commands?: SessionCommandDispatcher | undefined, workspaceRegistry?: TaskBoardWorkspaceRegistry | undefined, unavailableRetry?: {
|
|
62
63
|
attempts?: number;
|
|
63
64
|
backoffMs?: number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.11",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dsh": {
|
|
25
25
|
"engines": {
|
|
26
|
-
"dsh": ">=0.1.2-alpha.
|
|
26
|
+
"dsh": ">=0.1.2-alpha.3"
|
|
27
27
|
},
|
|
28
28
|
"bundle": {
|
|
29
29
|
"patch": "./cordis.patch.yml"
|
|
@@ -48,23 +48,23 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
51
|
-
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.
|
|
52
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.2-alpha.
|
|
53
|
-
"@deepseek-ai/dsh-api-gateway": "^0.1.2-alpha.
|
|
54
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.
|
|
55
|
-
"@deepseek-ai/dsh-workspace": "^0.1.2-alpha.
|
|
56
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.2-alpha.
|
|
57
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-alpha.
|
|
58
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.
|
|
59
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.
|
|
60
|
-
"@deepseek-ai/dsh-client-store": "^0.1.2-alpha.
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.
|
|
64
|
-
"@deepseek-ai/dsh-commands": "^0.1.2-alpha.
|
|
65
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.
|
|
66
|
-
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.
|
|
67
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.
|
|
51
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.3",
|
|
52
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.2-alpha.3",
|
|
53
|
+
"@deepseek-ai/dsh-api-gateway": "^0.1.2-alpha.3",
|
|
54
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.3",
|
|
55
|
+
"@deepseek-ai/dsh-workspace": "^0.1.2-alpha.3",
|
|
56
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.2-alpha.3",
|
|
57
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-alpha.3",
|
|
58
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.3",
|
|
59
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.3",
|
|
60
|
+
"@deepseek-ai/dsh-client-store": "^0.1.2-alpha.3",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.3",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.3",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.3",
|
|
64
|
+
"@deepseek-ai/dsh-commands": "^0.1.2-alpha.3",
|
|
65
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.3",
|
|
66
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.3",
|
|
67
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.3",
|
|
68
68
|
"@types/node": "^22.20.0",
|
|
69
69
|
"@types/react": "~18.3.1",
|
|
70
70
|
"@types/react-dom": "^18.3.5",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { createRoot, type Root } from 'react-dom/client'
|
|
16
16
|
import type { BoardController } from '../core/controller.ts'
|
|
17
17
|
import { TaskBoard } from './board/TaskBoard.tsx'
|
|
18
|
+
import type { LocaleRefreshSource } from './sidebar-entry.ts'
|
|
18
19
|
import css from './board.module.css'
|
|
19
20
|
|
|
20
21
|
/** The injected board container (kept in the DOM, hidden when inactive). */
|
|
@@ -37,11 +38,19 @@ function conversationColumn(): HTMLElement | undefined {
|
|
|
37
38
|
* Mount the board React tree into the center column and bind its visibility
|
|
38
39
|
* to the controller's boardOpen state.
|
|
39
40
|
* @param controller - the board controller driving the view.
|
|
41
|
+
* @param locale - locale-change source; when given, re-renders a mounted board
|
|
42
|
+
* on a Language switch.
|
|
40
43
|
* @returns disposer unmounting the tree and restoring the column.
|
|
41
44
|
*/
|
|
42
|
-
export function mountBoard(controller: BoardController): () => void {
|
|
45
|
+
export function mountBoard(controller: BoardController, locale?: LocaleRefreshSource): () => void {
|
|
43
46
|
let root: Root | undefined
|
|
44
47
|
let container: HTMLDivElement | undefined
|
|
48
|
+
let unsubscribeLocale: (() => void) | undefined
|
|
49
|
+
try {
|
|
50
|
+
unsubscribeLocale = locale?.subscribe(() => {
|
|
51
|
+
if (root !== undefined) root.render(<TaskBoard controller={controller} />)
|
|
52
|
+
})
|
|
53
|
+
} catch { /* locale service absent: the board follows its next natural re-render */ }
|
|
45
54
|
|
|
46
55
|
const ensure = (): void => {
|
|
47
56
|
if (container !== undefined) {
|
|
@@ -106,6 +115,7 @@ export function mountBoard(controller: BoardController): () => void {
|
|
|
106
115
|
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
107
116
|
waitObserver.disconnect()
|
|
108
117
|
unsubscribe()
|
|
118
|
+
unsubscribeLocale?.()
|
|
109
119
|
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
110
120
|
root?.unmount()
|
|
111
121
|
root = undefined
|
|
@@ -99,7 +99,8 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/* Collapsed rail: icon-only, centered, matching the shell's 56px rail. */
|
|
102
|
-
[data-dsh-frame][data-sidebar-collapsed] .entry
|
|
102
|
+
:global([data-dsh-frame][data-sidebar-collapsed]) .entry,
|
|
103
|
+
:global([data-sidebar-collapsed]) .entry {
|
|
103
104
|
justify-content: center;
|
|
104
105
|
padding: 0;
|
|
105
106
|
width: 36px;
|
|
@@ -108,7 +109,8 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
108
109
|
border-radius: 50%;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
[data-dsh-frame][data-sidebar-collapsed] .entryLabel
|
|
112
|
+
:global([data-dsh-frame][data-sidebar-collapsed]) .entryLabel,
|
|
113
|
+
:global([data-sidebar-collapsed]) .entryLabel {
|
|
112
114
|
display: none;
|
|
113
115
|
}
|
|
114
116
|
|
package/src/client/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { claimTaskboardApply, releaseTaskboardApply } from './apply-guard.ts'
|
|
|
27
27
|
import { mountBoard } from './board-mount.tsx'
|
|
28
28
|
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
29
29
|
import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardSettings } from './TaskBoardSettingsCard.tsx'
|
|
30
|
-
import { en, zh, type TaskBoardKey } from './locales.ts'
|
|
30
|
+
import { en, zh, setRuntimeTranslate, type TaskBoardKey } from './locales.ts'
|
|
31
31
|
import { HttpTaskBoardHostTransport } from './host-api.ts'
|
|
32
32
|
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
33
33
|
|
|
@@ -157,6 +157,12 @@ export function apply(ctx: ClientContext): void {
|
|
|
157
157
|
}
|
|
158
158
|
}, 'task-board: dictionaries')
|
|
159
159
|
|
|
160
|
+
// Wire the SDK translate seat into the module-level t (sidebar row and
|
|
161
|
+
// other plain-DOM callers): reads the active locale at call time, so they
|
|
162
|
+
// follow the Language setting without a reload. The register effect above
|
|
163
|
+
// guarantees the dictionaries exist before the first read.
|
|
164
|
+
try { setRuntimeTranslate(ctx.locale.bind(NS)) } catch { /* locale missing: document-language fallback stays */ }
|
|
165
|
+
|
|
160
166
|
// Plugin configuration card: one staged form over the `task-board` settings
|
|
161
167
|
// namespace, contributed to the Web UI plugin group.
|
|
162
168
|
const binder = ctx.get('webUiSettings') ?? ctx.settingsScope
|
|
@@ -201,7 +207,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
201
207
|
transport: new HttpTaskBoardHostTransport(),
|
|
202
208
|
sessions: {
|
|
203
209
|
list: sessions.list,
|
|
204
|
-
open: id => sessions.open(id as
|
|
210
|
+
open: id => sessions.open(id as never),
|
|
205
211
|
},
|
|
206
212
|
})
|
|
207
213
|
controller.start()
|
|
@@ -246,8 +252,8 @@ export function apply(ctx: ClientContext): void {
|
|
|
246
252
|
void pushPresetOptions()
|
|
247
253
|
disposers.push(ctx.on('connection/reset', () => { void pushPresetOptions() }))
|
|
248
254
|
try {
|
|
249
|
-
disposers.push(mountSidebarEntry(controller))
|
|
250
|
-
disposers.push(mountBoard(controller))
|
|
255
|
+
disposers.push(mountSidebarEntry(controller, ctx.locale))
|
|
256
|
+
disposers.push(mountBoard(controller, ctx.locale))
|
|
251
257
|
} catch (error) {
|
|
252
258
|
// DOM failures degrade the board, never the GUI.
|
|
253
259
|
console.error('[dsh-task-board] mount failed:', error)
|
package/src/client/locales.ts
CHANGED
|
@@ -307,8 +307,23 @@ export function dictionary(): Record<TaskBoardKey, string> {
|
|
|
307
307
|
return lang.toLowerCase().startsWith('en') ? en : zh
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
/**
|
|
311
|
+
* SDK translate seat wired by the browser apply() once ctx.locale is bound
|
|
312
|
+
* (setRuntimeTranslate). When present it reads the ACTIVE locale at call
|
|
313
|
+
* time, so plain-DOM surfaces (sidebar row, toggles) follow a runtime
|
|
314
|
+
* language switch; the document-language pick above stays only as the
|
|
315
|
+
* unwired fallback (locale service absent, module-scope early callers).
|
|
316
|
+
*/
|
|
317
|
+
let runtimeT: ((key: TaskBoardKey, params?: Record<string, string>) => string) | undefined
|
|
318
|
+
|
|
319
|
+
/** Wire the SDK translate seat; pass undefined to restore the document-language pick. */
|
|
320
|
+
export function setRuntimeTranslate(t: ((key: TaskBoardKey, params?: Record<string, string>) => string) | undefined): void {
|
|
321
|
+
runtimeT = t
|
|
322
|
+
}
|
|
323
|
+
|
|
310
324
|
/** Translate a key with optional {name} template params. */
|
|
311
325
|
export function t(key: TaskBoardKey, params?: Record<string, string>): string {
|
|
326
|
+
if (runtimeT !== undefined) return runtimeT(key, params)
|
|
312
327
|
let text: string = dictionary()[key]
|
|
313
328
|
if (params !== undefined) {
|
|
314
329
|
for (const [name, value] of Object.entries(params)) {
|
|
@@ -36,6 +36,14 @@ export interface SidebarEntryOptions {
|
|
|
36
36
|
label(): string
|
|
37
37
|
/** Optional localized tooltip (title attribute). */
|
|
38
38
|
tooltip?(): string
|
|
39
|
+
/**
|
|
40
|
+
* Optional locale-change subscription: re-applies label / aria-label /
|
|
41
|
+
* tooltip whenever the active locale changes. Plain-DOM rows would
|
|
42
|
+
* otherwise keep the label captured at mount; pass the SDK locale runtime
|
|
43
|
+
* subscription (ctx.locale.subscribe) so the row follows the language
|
|
44
|
+
* switch without a reload.
|
|
45
|
+
*/
|
|
46
|
+
refresh?: { subscribe(listener: () => void): () => void }
|
|
39
47
|
/** Click action (open/toggle the owning panel). */
|
|
40
48
|
onToggle(): void
|
|
41
49
|
/** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
|
|
@@ -76,7 +84,7 @@ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
|
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
79
|
-
function createEntry(options: SidebarEntryOptions): HTMLButtonElement {
|
|
87
|
+
function createEntry(options: SidebarEntryOptions): { entry: HTMLButtonElement; applyLabel: () => void } {
|
|
80
88
|
const entry = document.createElement('button')
|
|
81
89
|
entry.type = 'button'
|
|
82
90
|
entry.setAttribute(options.rowAttribute, '')
|
|
@@ -85,12 +93,20 @@ function createEntry(options: SidebarEntryOptions): HTMLButtonElement {
|
|
|
85
93
|
entry.setAttribute('data-dsh-part', 'sidebar-entry')
|
|
86
94
|
}
|
|
87
95
|
entry.className = options.css['entry'] ?? ''
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
const labelSpan = document.createElement('span')
|
|
97
|
+
labelSpan.className = options.css['entryLabel'] ?? ''
|
|
98
|
+
const iconSpan = document.createElement('span')
|
|
99
|
+
iconSpan.className = options.css['entryIcon'] ?? ''
|
|
100
|
+
iconSpan.innerHTML = options.icon
|
|
101
|
+
entry.append(iconSpan, labelSpan)
|
|
102
|
+
const applyLabel = (): void => {
|
|
103
|
+
entry.setAttribute('aria-label', options.label())
|
|
104
|
+
if (options.tooltip !== undefined) entry.setAttribute('title', options.tooltip())
|
|
105
|
+
labelSpan.textContent = options.label()
|
|
106
|
+
}
|
|
107
|
+
applyLabel()
|
|
92
108
|
entry.addEventListener('click', options.onToggle)
|
|
93
|
-
return entry
|
|
109
|
+
return { entry, applyLabel }
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
@@ -132,9 +148,18 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
|
|
|
132
148
|
if (typeof document !== 'undefined' && document.querySelector(options.rowSelector) !== null) {
|
|
133
149
|
return () => {}
|
|
134
150
|
}
|
|
135
|
-
const entry = createEntry(options)
|
|
151
|
+
const { entry, applyLabel } = createEntry(options)
|
|
136
152
|
let root: HTMLElement | undefined
|
|
137
153
|
let placed = false
|
|
154
|
+
let unsubscribeRefresh: (() => void) | undefined
|
|
155
|
+
if (options.refresh !== undefined) {
|
|
156
|
+
try {
|
|
157
|
+
unsubscribeRefresh = options.refresh.subscribe(applyLabel)
|
|
158
|
+
} catch {
|
|
159
|
+
// A throwing subscription must not break the mount; the label stays at
|
|
160
|
+
// its initial value and the next reload resolves it again.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
138
163
|
|
|
139
164
|
const tryPlace = (): void => {
|
|
140
165
|
if (root !== undefined && !root.isConnected) {
|
|
@@ -202,6 +227,7 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
|
|
|
202
227
|
return () => {
|
|
203
228
|
waitObserver.disconnect()
|
|
204
229
|
rootObserver.disconnect()
|
|
230
|
+
unsubscribeRefresh?.()
|
|
205
231
|
unsubscribeActive?.()
|
|
206
232
|
entry.remove()
|
|
207
233
|
}
|
|
@@ -22,13 +22,18 @@ export const ENTRY_SELECTOR = '[data-dsh-taskboard-entry]'
|
|
|
22
22
|
/** Inline icon normalized to the shell's 18px navigation glyph size. */
|
|
23
23
|
const ICON = '<svg viewBox="0 0 16 16" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M6.5 6.5v7"/></svg>'
|
|
24
24
|
|
|
25
|
+
/** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
|
|
26
|
+
export interface LocaleRefreshSource { subscribe(listener: () => void): () => void }
|
|
27
|
+
|
|
25
28
|
/**
|
|
26
29
|
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
27
30
|
* on later React re-renders.
|
|
28
31
|
* @param controller - the board controller the entry toggles.
|
|
32
|
+
* @param locale - locale-change source; when given, re-applies the label on
|
|
33
|
+
* a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
|
|
29
34
|
* @returns disposer removing the entry and its observers.
|
|
30
35
|
*/
|
|
31
|
-
export function mountSidebarEntry(controller: BoardController): () => void {
|
|
36
|
+
export function mountSidebarEntry(controller: BoardController, locale?: LocaleRefreshSource): () => void {
|
|
32
37
|
return mountSharedSidebarEntry({
|
|
33
38
|
rowAttribute: 'data-dsh-taskboard-entry',
|
|
34
39
|
rowSelector: ENTRY_SELECTOR,
|
|
@@ -36,6 +41,7 @@ export function mountSidebarEntry(controller: BoardController): () => void {
|
|
|
36
41
|
icon: ICON,
|
|
37
42
|
css,
|
|
38
43
|
label: () => t('entry.label'),
|
|
44
|
+
refresh: locale === undefined ? undefined : { subscribe: (listener) => locale.subscribe(listener) },
|
|
39
45
|
onToggle: () => { controller.toggleBoard() },
|
|
40
46
|
position: 'before',
|
|
41
47
|
familySelectors: ['[data-dsh-taskboard-entry]', '[data-dsh-ssh-entry]'],
|
package/src/core/tasks.ts
CHANGED
|
@@ -361,7 +361,8 @@ export function settleExecution(
|
|
|
361
361
|
const settled: ExecutionRecord = { ...execution, endedAt: now, result: outcome, error }
|
|
362
362
|
const executions = [...task.executions]
|
|
363
363
|
executions[index] = settled
|
|
364
|
-
const status: TaskStatus = outcome === 'succeeded'
|
|
364
|
+
const status: TaskStatus = outcome === 'succeeded'
|
|
365
|
+
? (task.schedule?.enabled ? 'todo' : 'done')
|
|
365
366
|
: outcome === 'failed' ? 'failed'
|
|
366
367
|
: task.status === 'running' ? 'todo' : task.status
|
|
367
368
|
return { ...task, status, updatedAt: now, executions }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/run-guarded.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Async-boundary guard shared by the plugin family's Host halves: every
|
|
4
|
+
* fire-and-forget promise chain and callback the host runtime does not own
|
|
5
|
+
* (route handlers, timers, event listeners, spawned work) funnels through
|
|
6
|
+
* these helpers. The dsh host installs a process-level fail-loud guard that
|
|
7
|
+
* turns ANY unhandled promise rejection into a whole-process exit — one
|
|
8
|
+
* plugin's stray rejection would otherwise take every plugin down. These
|
|
9
|
+
* helpers exist so that failure mode is structurally impossible in family
|
|
10
|
+
* code: the rejection becomes a logged error at the plugin boundary instead.
|
|
11
|
+
*
|
|
12
|
+
* Complements the aggregate's shell isolation (packages/dsh-web-all): the
|
|
13
|
+
* shell contains import/activation failures at boot; runGuarded contains
|
|
14
|
+
* run-time failures after activation.
|
|
15
|
+
* @module dsh-web-shared/host/run-guarded
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Format one failure line for logging. */
|
|
19
|
+
function formatFailure(label: string, error: unknown): Error {
|
|
20
|
+
return new Error(`[${label}] unhandled async failure: ${error instanceof Error ? error.stack ?? error.message : String(error)}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run one async operation, logging (never propagating) a rejection. Use for
|
|
25
|
+
* promises whose lifecycle the caller does not await: spawned work, retry
|
|
26
|
+
* loops, background flushes. Returns the original promise so callers can
|
|
27
|
+
* still chain when they want to.
|
|
28
|
+
* @param promise - the work to shield; any thenable.
|
|
29
|
+
* @param label - log prefix naming the work site.
|
|
30
|
+
* @param log - error sink; defaults to console.error.
|
|
31
|
+
* @returns the input promise (rejection already consumed).
|
|
32
|
+
*/
|
|
33
|
+
export function runGuarded<T>(promise: PromiseLike<T>, label: string, log: (error: unknown) => void = console.error): PromiseLike<T> {
|
|
34
|
+
void Promise.resolve(promise).catch(error => {
|
|
35
|
+
log(formatFailure(label, error))
|
|
36
|
+
})
|
|
37
|
+
return promise
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wrap one callback so every invocation is individually guarded: a rejection
|
|
42
|
+
* inside one call is logged and swallowed instead of escaping into whatever
|
|
43
|
+
* infrastructure invoked the callback (HTTP server, EventEmitter, interval).
|
|
44
|
+
* Sync throws are caught identically; a returned promise is replaced by
|
|
45
|
+
* `undefined` after guarding (callers that need the original rejection should
|
|
46
|
+
* await inside their own try/catch instead).
|
|
47
|
+
* @param label - log prefix naming the callback site.
|
|
48
|
+
* @param handler - the work to guard.
|
|
49
|
+
* @param log - error sink; defaults to console.error.
|
|
50
|
+
* @returns a wrapped callback with the same parameter list.
|
|
51
|
+
*/
|
|
52
|
+
export function guardedHandler<A extends unknown[]>(
|
|
53
|
+
label: string,
|
|
54
|
+
handler: (...args: A) => unknown,
|
|
55
|
+
log: (error: unknown) => void = console.error,
|
|
56
|
+
): (...args: A) => unknown {
|
|
57
|
+
return (...args: A) => {
|
|
58
|
+
try {
|
|
59
|
+
const result = handler(...args)
|
|
60
|
+
if (isPromiseLike(result)) {
|
|
61
|
+
void Promise.resolve(result).catch(error => {
|
|
62
|
+
log(formatFailure(label, error))
|
|
63
|
+
})
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
return result
|
|
67
|
+
} catch (error) {
|
|
68
|
+
log(formatFailure(label, error))
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
75
|
+
return typeof value === 'object' && value !== null && typeof (value as PromiseLike<unknown>).then === 'function'
|
|
76
|
+
}
|
package/src/host-runner.ts
CHANGED
|
@@ -40,6 +40,11 @@ function isServiceUnavailable(error: unknown): boolean {
|
|
|
40
40
|
return code === 'service-unavailable' || code === 'gateway/service-unavailable'
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function isInvocationUnavailable(error: unknown): boolean {
|
|
44
|
+
const code = (error as { code?: unknown }).code
|
|
45
|
+
return code === 'invocation-unavailable' || code === 'gateway/invocation-unavailable'
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
const SERVICE_UNAVAILABLE_ATTEMPTS = 5
|
|
44
49
|
const SERVICE_UNAVAILABLE_BACKOFF_MS = 2_000
|
|
45
50
|
|
|
@@ -145,6 +150,7 @@ export class HostExecutionRunner {
|
|
|
145
150
|
private readonly scanMemos = new Map<string, number>()
|
|
146
151
|
private readonly unavailableAttempts: number
|
|
147
152
|
private readonly unavailableBackoffMs: number
|
|
153
|
+
private unsupportedSessionListWarned = false
|
|
148
154
|
|
|
149
155
|
constructor(
|
|
150
156
|
private readonly gateway: SessionGateway | TypertGateway,
|
|
@@ -214,6 +220,13 @@ export class HostExecutionRunner {
|
|
|
214
220
|
const response = await this.invoke('session', 'list', {}) as SessionListValue
|
|
215
221
|
return { known: true, count: response.items.filter(item => item.running).length, items: response.items as SessionSummary[] }
|
|
216
222
|
} catch (error) {
|
|
223
|
+
if (isInvocationUnavailable(error)) {
|
|
224
|
+
if (!this.unsupportedSessionListWarned) {
|
|
225
|
+
this.unsupportedSessionListWarned = true
|
|
226
|
+
console.warn('[dsh-task-board] DSH runtime session endpoint unavailable (requires DSH >= 0.1.2-alpha.2); task board roster auto-discovery is disabled', error)
|
|
227
|
+
}
|
|
228
|
+
return { known: false }
|
|
229
|
+
}
|
|
217
230
|
if (!isServiceUnavailable(error) || attempt >= this.unavailableAttempts) {
|
|
218
231
|
console.error('[dsh-task-board] session/list failed; treating the host session roster as unknown', error)
|
|
219
232
|
return { known: false }
|
|
@@ -233,6 +246,13 @@ export class HostExecutionRunner {
|
|
|
233
246
|
try {
|
|
234
247
|
response = await this.invoke('session', 'list', {}) as SessionListValue
|
|
235
248
|
} catch (error) {
|
|
249
|
+
if (isInvocationUnavailable(error)) {
|
|
250
|
+
if (!this.unsupportedSessionListWarned) {
|
|
251
|
+
this.unsupportedSessionListWarned = true
|
|
252
|
+
console.warn('[dsh-task-board] DSH runtime session endpoint unavailable (requires DSH >= 0.1.2-alpha.2); task board roster auto-discovery is disabled', error)
|
|
253
|
+
}
|
|
254
|
+
return { outcome: 'pending' }
|
|
255
|
+
}
|
|
236
256
|
console.warn('[dsh-task-board] session/list failed during execution inspection; keeping the outcome pending', error)
|
|
237
257
|
return { outcome: 'pending' }
|
|
238
258
|
}
|