Skip to content

Commit 06480d7

Browse files
authored
feat(dashboard): stitch session dashboards end-to-end — live provider, show_widget pin, board commands (#111218)
* feat(dashboard): pin canvas widgets through board domain * feat(control-ui): wire session dashboards end to end * fix(control-ui): preserve direct reset confirmation * fix(control-ui): localize dashboard default tab * test(dashboard): satisfy changed typecheck gate * fix(dashboard): satisfy changed lint gate * fix(dashboard): harden pinned widget lifecycle * fix(dashboard): use extracted mock normalizer * test(dashboard): reconcile hardened board admission tests * fix(dashboard): align grant and canvas protocol requests * fix(dashboard): preserve refresh and pin invariants * fix(boards): treat missing board tables as empty on read-only paths * fix(boards): accept read-only handles in board table probe * fix(control-ui): preserve virtual board widget types * fix(dashboard): align view types with protocol * fix(dashboard): keep resolved board view typed * chore(canvas): drop test-only html read wrapper * chore(release): defer dashboard note generation * fix(protocol): align board widget put params type * fix(control-ui): preserve queued reset approval * fix(control-ui): gate dashboard pinning capability * chore(test): isolate dashboard capability fixture * fix(control-ui): refresh dashboard pin affordance
1 parent 431da14 commit 06480d7

55 files changed

Lines changed: 3042 additions & 384 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,24 @@ public struct BoardWidgetMcpAppContent: Codable, Sendable {
546546
}
547547
}
548548

549+
public struct BoardCanvasDocumentSource: Codable, Sendable {
550+
public let kind: String
551+
public let docid: String
552+
553+
public init(
554+
kind: String,
555+
docid: String)
556+
{
557+
self.kind = kind
558+
self.docid = docid
559+
}
560+
561+
private enum CodingKeys: String, CodingKey {
562+
case kind
563+
case docid = "docId"
564+
}
565+
}
566+
549567
public struct BoardGetParams: Codable, Sendable {
550568
public let sessionkey: String
551569

@@ -582,15 +600,15 @@ public struct BoardWidgetPutParams: Codable, Sendable {
582600
public let sessionkey: String
583601
public let name: String
584602
public let title: String?
585-
public let content: BoardWidgetContent
603+
public let content: BoardWidgetPutContent
586604
public let placement: [String: AnyCodable]?
587605
public let declared: [String: AnyCodable]?
588606

589607
public init(
590608
sessionkey: String,
591609
name: String,
592610
title: String? = nil,
593-
content: BoardWidgetContent,
611+
content: BoardWidgetPutContent,
594612
placement: [String: AnyCodable]? = nil,
595613
declared: [String: AnyCodable]? = nil)
596614
{
@@ -15558,6 +15576,40 @@ public enum BoardWidgetContent: Codable, Sendable {
1555815576
}
1555915577
}
1556015578

15579+
public enum BoardWidgetPutContent: Codable, Sendable {
15580+
case html(BoardWidgetHtmlContent)
15581+
case mcpApp(BoardWidgetMcpAppContent)
15582+
case canvasDoc(BoardCanvasDocumentSource)
15583+
15584+
private enum CodingKeys: String, CodingKey {
15585+
case discriminator = "kind"
15586+
}
15587+
15588+
public init(from decoder: Decoder) throws {
15589+
let container = try decoder.container(keyedBy: CodingKeys.self)
15590+
let discriminator = try container.decode(String.self, forKey: .discriminator)
15591+
switch discriminator {
15592+
case "html": self = try .html(BoardWidgetHtmlContent(from: decoder))
15593+
case "mcp-app": self = try .mcpApp(BoardWidgetMcpAppContent(from: decoder))
15594+
case "canvas-doc": self = try .canvasDoc(BoardCanvasDocumentSource(from: decoder))
15595+
default:
15596+
throw DecodingError.dataCorruptedError(
15597+
forKey: .discriminator,
15598+
in: container,
15599+
debugDescription: "Unknown BoardWidgetPutContent discriminator value"
15600+
)
15601+
}
15602+
}
15603+
15604+
public func encode(to encoder: Encoder) throws {
15605+
switch self {
15606+
case .html(let value): try value.encode(to: encoder)
15607+
case .mcpApp(let value): try value.encode(to: encoder)
15608+
case .canvasDoc(let value): try value.encode(to: encoder)
15609+
}
15610+
}
15611+
}
15612+
1556115613
public enum BoardCommand: Codable, Sendable {
1556215614
case focusTab(BoardFocusTabCommand)
1556315615
case setChatDock(BoardSetChatDockCommand)

config/knip.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,9 @@ const config = {
358358
"src/boards/board-notices.ts": ["exports"],
359359
"src/boards/board-store.ts": ["exports"],
360360
"src/gateway/board-view-ticket.ts": ["exports"],
361+
// GatewayBoardProvider and boardExists are constructed/asserted by the
362+
// focused Control UI provider tests, not by a separate production module.
363+
"ui/src/lib/board/provider.ts": ["exports"],
361364
},
362365
workspaces: {
363366
".": {

docs/tools/show-widget.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ read_when:
1515

1616
When the agent calls `show_widget`, OpenClaw core wraps `widget_code` in a minimal HTML document, stores it as a Canvas document, and returns a preview handle. The Control UI renders that handle as a sandboxed iframe directly under the tool call, while native apps use an isolated web view. Both restore the widget after history reload.
1717

18+
In Control UI sessions, a Canvas widget can also be pinned to the session dashboard. Set `pin: true` in the tool call, or use **Pin to dashboard** on an existing transcript widget. Pinning reuses the exact hosted document; it does not fetch widget HTML through the browser.
19+
1820
For browser embedding, the wrapper document injects four small host bridges around the widget code:
1921

2022
- A size reporter posts the rendered content height to the embedding chat, which clamps it and fits the iframe (160 to 1200 pixels).
@@ -87,7 +89,15 @@ Both implementations use the same required fields:
8789

8890
Discord also accepts optional `button_label` text for the Activity launch button. The Canvas schema intentionally omits this Discord-only field.
8991

90-
The core result includes a Canvas preview handle, so the Control UI and supported native apps render the widget directly from the tool call and restore it after history reload. Discord returns the stored widget and posted-message identifiers.
92+
The core Canvas tool accepts these optional dashboard placement fields:
93+
94+
- `pin`: also place the widget on the session dashboard.
95+
- `name`: stable widget name; defaults to a slug of `title`.
96+
- `tab`: destination tab slug.
97+
- `size`: one of `sm`, `md`, `lg`, `xl`, or `full`.
98+
- `after`: sibling widget name after which to place the widget.
99+
100+
The core result includes a Canvas preview handle, so the Control UI and supported native apps render the widget directly from the tool call and restore it after history reload. Pinned results also retain the board widget name so the Control UI does not offer a duplicate pin after transcript reload. Discord returns the stored widget and posted-message identifiers.
91101

92102
`discord_widget` remains registered as a deprecated alias for one release. New agent calls should use `show_widget`.
93103

packages/gateway-protocol/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
BoardEventParamsSchema,
2323
BoardGetParamsSchema,
2424
BoardUpdateParamsSchema,
25+
BoardWidgetContentSchema,
2526
BoardWidgetGrantParamsSchema,
2627
BoardWidgetPutParamsSchema,
2728
AgentEventSchema,
@@ -668,6 +669,7 @@ export const validateAgentsListParams = lazyCompile(AgentsListParamsSchema);
668669
export const validateWorktreesListParams = lazyCompile(WorktreesListParamsSchema);
669670
export const validateBoardGetParams = lazyCompile(BoardGetParamsSchema);
670671
export const validateBoardUpdateParams = lazyCompile(BoardUpdateParamsSchema);
672+
export const validateBoardWidgetContent = lazyCompile(BoardWidgetContentSchema);
671673
export const validateBoardWidgetPutParams = lazyCompile(BoardWidgetPutParamsSchema);
672674
export const validateBoardWidgetGrantParams = lazyCompile(BoardWidgetGrantParamsSchema);
673675
export const validateBoardEventParams = lazyCompile(BoardEventParamsSchema);

packages/gateway-protocol/src/schema/board.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { Value } from "typebox/value";
22
import { describe, expect, it } from "vitest";
3-
import { BoardSnapshotSchema, BoardWidgetGrantParamsSchema } from "./board.js";
3+
import {
4+
BoardSnapshotSchema,
5+
BoardWidgetGrantParamsSchema,
6+
BoardWidgetPutParamsSchema,
7+
} from "./board.js";
48

59
describe("BoardSnapshotSchema", () => {
610
it("accepts optional HTML widget view metadata", () => {
@@ -37,6 +41,40 @@ describe("BoardSnapshotSchema", () => {
3741
}),
3842
).toBe(false);
3943
});
44+
45+
it("accepts declared grant summaries", () => {
46+
const widget = {
47+
name: "status",
48+
tabId: "main",
49+
contentKind: "mcp-app",
50+
sizeW: 6,
51+
sizeH: 4,
52+
position: 0,
53+
grantState: "pending",
54+
declaredSummary: ["Network: api.example.com", "Tools: lookup"],
55+
revision: 1,
56+
};
57+
const snapshot = {
58+
sessionKey: "agent:main:main",
59+
revision: 1,
60+
tabs: [{ tabId: "main", title: "Main", position: 0, chatDock: "right" }],
61+
widgets: [widget],
62+
};
63+
64+
expect(Value.Check(BoardSnapshotSchema, snapshot)).toBe(true);
65+
});
66+
});
67+
68+
describe("BoardWidgetPutParamsSchema", () => {
69+
it("accepts a gateway-resolved canvas document source", () => {
70+
expect(
71+
Value.Check(BoardWidgetPutParamsSchema, {
72+
sessionKey: "agent:main:main",
73+
name: "status",
74+
content: { kind: "canvas-doc", docId: "cv_status" },
75+
}),
76+
).toBe(true);
77+
});
4078
});
4179

4280
describe("BoardWidgetGrantParamsSchema", () => {

packages/gateway-protocol/src/schema/board.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,24 @@ export const BoardWidgetContentSchema = Type.Union([
138138
]);
139139
export type BoardWidgetContent = Static<typeof BoardWidgetContentSchema>;
140140

141+
export const BoardCanvasDocumentSourceSchema = closedObject({
142+
kind: Type.Literal("canvas-doc"),
143+
docId: NonEmptyString,
144+
});
145+
export type BoardCanvasDocumentSource = Static<typeof BoardCanvasDocumentSourceSchema>;
146+
147+
export const BoardWidgetPutContentSchema = Type.Union([
148+
BoardWidgetHtmlContentSchema,
149+
BoardWidgetMcpAppContentSchema,
150+
BoardCanvasDocumentSourceSchema,
151+
]);
152+
export type BoardWidgetPutContent = Static<typeof BoardWidgetPutContentSchema>;
153+
141154
export const BoardWidgetPutParamsSchema = closedObject({
142155
sessionKey: NonEmptyString,
143156
name: BoardWidgetNameSchema,
144157
title: Type.Optional(Type.String({ minLength: 1, maxLength: 80 })),
145-
content: BoardWidgetContentSchema,
158+
content: BoardWidgetPutContentSchema,
146159
placement: Type.Optional(
147160
closedObject({
148161
tabId: Type.Optional(BoardTabIdSchema),
@@ -158,6 +171,10 @@ export const BoardWidgetPutParamsSchema = closedObject({
158171
),
159172
});
160173
export type BoardWidgetPutParams = Static<typeof BoardWidgetPutParamsSchema>;
174+
/** Materialized input accepted by the board store after gateway source resolution. */
175+
export type BoardWidgetMaterializedPutParams = Omit<BoardWidgetPutParams, "content"> & {
176+
content: BoardWidgetContent;
177+
};
161178

162179
export const BoardWidgetGrantParamsSchema = closedObject({
163180
sessionKey: NonEmptyString,

packages/gateway-protocol/src/schema/frames.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { GatewayClientIdSchema, GatewayClientModeSchema, NonEmptyString } from "
66
import { SnapshotSchema, StateVersionSchema } from "./snapshot.js";
77

88
export const GATEWAY_SERVER_CAPS = {
9+
BOARD_WIDGET_PUT_CANVAS_DOC: "board-widget-put-canvas-doc",
910
CHAT_SEND_ROUTING_CONTRACT: "chat-send-routing-contract",
1011
SYSTEM_AGENT_SETUP_MODEL_REF: "openclaw-setup-model-ref",
1112
} as const;

packages/gateway-protocol/src/schema/protocol-schemas.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ import {
152152
} from "./audit-activity.js";
153153
import { AuditEventSchema, AuditListParamsSchema, AuditListResultSchema } from "./audit.js";
154154
import {
155+
BoardCanvasDocumentSourceSchema,
155156
BoardChangedEventSchema,
156157
BoardCommandEventSchema,
157158
BoardCommandSchema,
@@ -173,6 +174,7 @@ import {
173174
BoardWidgetHtmlContentSchema,
174175
BoardWidgetMcpAppContentSchema,
175176
BoardWidgetMoveOpSchema,
177+
BoardWidgetPutContentSchema,
176178
BoardWidgetPutParamsSchema,
177179
BoardWidgetRemoveOpSchema,
178180
BoardWidgetResizeOpSchema,
@@ -600,7 +602,9 @@ export const ProtocolSchemas = {
600602
BoardMcpAppDescriptor: BoardMcpAppDescriptorSchema,
601603
BoardWidgetHtmlContent: BoardWidgetHtmlContentSchema,
602604
BoardWidgetMcpAppContent: BoardWidgetMcpAppContentSchema,
605+
BoardCanvasDocumentSource: BoardCanvasDocumentSourceSchema,
603606
BoardWidgetContent: BoardWidgetContentSchema,
607+
BoardWidgetPutContent: BoardWidgetPutContentSchema,
604608
BoardGetParams: BoardGetParamsSchema,
605609
BoardUpdateParams: BoardUpdateParamsSchema,
606610
BoardWidgetPutParams: BoardWidgetPutParamsSchema,

src/agents/openclaw-tools.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,7 @@ export function createOpenClawTools(
546546
createShowWidgetTool({
547547
sessionId: options?.sessionId,
548548
agentId: sessionAgentId,
549+
agentSessionKey: options?.runSessionKey ?? options?.agentSessionKey,
549550
}),
550551
]),
551552
...collectPresentOpenClawTools([heartbeatTool]),

src/boards/board-store.parity.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ describe("SqliteBoardStore persistence", () => {
256256
resolveSession: () => ({ agentId: "main", sessionKey }),
257257
env,
258258
});
259+
// Reads before any write must see "no boards", not "no such table".
260+
expect(store.getSnapshot(sessionKey)).toMatchObject({ revision: 0, tabs: [], widgets: [] });
261+
expect(store.readWidgetHtml(sessionKey, "status")).toBeUndefined();
262+
expect(store.listSessionsWithBoards()).toEqual([]);
259263
expect(() =>
260264
store.putWidget({
261265
sessionKey,

0 commit comments

Comments
 (0)