Skip to content

Commit 73eea89

Browse files
committed
Release 1.0.12: Timeline hides control-plane x.ai extension notifications
- describeUpdate default branch marks unclassified x.ai/* and _x.ai/* methods (models/settings/MCP/announcements sync, mcp_initialized, prompt acks) as hidden; the shared shouldDropUpdate gate drops them from the live stream and disk hydrate alike — no more raw channel-name cards - agent-notification listener now applies the same drop gate as the session/update stream (hidden session-event variants inside x.ai/session_notification no longer render as cards); dead lifecycle fallback branch and its imports removed - tests: control-plane channels assert hidden + dropped end to end; lifecycle notifications keep the rendered path
1 parent 8198d17 commit 73eea89

9 files changed

Lines changed: 124 additions & 32 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pinkcode",
3-
"version": "1.0.11",
3+
"version": "1.0.12",
44
"description": "Desktop mission control for Grok agents.",
55
"type": "module",
66
"keywords": [

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pinkcode"
3-
version = "1.0.11"
3+
version = "1.0.12"
44
description = "Desktop control plane for Grok Build multi-task observability"
55
authors = ["PinkCode"]
66
license = "Apache-2.0"

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "PinkCode",
44
"mainBinaryName": "PinkCode",
5-
"version": "1.0.11",
5+
"version": "1.0.12",
66
"identifier": "com.pinkcode.app",
77
"build": {
88
"beforeDevCommand": "npm run dev",

src/hooks/liveTimeline.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {
1111
reduceShellUpdate,
1212
settleLifecycleItems,
1313
settleStreamingItems,
14+
shouldDropUpdate,
1415
type UpdateDescription,
1516
} from "./liveTimeline";
17+
import { describeUpdate } from "../utils/format";
1618
import type { TimelineItem } from "../types";
1719

1820
describe("live timeline reducer", () => {
@@ -750,3 +752,35 @@ describe("live timeline reducer", () => {
750752
expect(state.get("session")?.[0].shell?.output).toBe("first second");
751753
});
752754
});
755+
756+
describe("control-plane extension notification gate", () => {
757+
it("drops x.ai extension notifications end to end", () => {
758+
// describeUpdate marks them hidden; the shared gate (live stream and disk
759+
// hydrate) turns that into a drop before the reducer ever sees them.
760+
for (const method of [
761+
"_x.ai/mcp/servers_updated",
762+
"_x.ai/models/update",
763+
"_x.ai/announcements/update",
764+
"x.ai/mcp_initialized",
765+
]) {
766+
const desc = describeUpdate({ method, params: {} });
767+
expect(desc.hidden, `${method} must be hidden`).toBe(true);
768+
expect(shouldDropUpdate(desc), `${method} must be dropped`).toBe(true);
769+
}
770+
});
771+
772+
it("keeps lifecycle notifications on the rendered path", () => {
773+
const desc = describeUpdate({
774+
method: "x.ai/session_notification",
775+
params: {
776+
sessionId: "parent-1",
777+
update: {
778+
sessionUpdate: "subagent_spawned",
779+
child_session_id: "child-1",
780+
},
781+
},
782+
});
783+
expect(desc.hidden).toBeUndefined();
784+
expect(shouldDropUpdate(desc)).toBe(false);
785+
});
786+
});

src/hooks/useAgentEvents.ts

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ import type {
1212
import type { LocalSlashItem } from "../utils/localSlash";
1313
import { describeUpdate, extractUpdateTsMs } from "../utils/format";
1414
import {
15-
describeLifecycleNotification,
1615
describePendingInteractionNotification,
17-
isLifecycleNotificationMethod,
1816
type LifecycleDescription,
1917
type PendingInteractionKind,
2018
} from "../utils/subagentTasks";
@@ -477,43 +475,27 @@ export function useAgentEvents(
477475
return;
478476
}
479477

480-
// Non-lifecycle notifications (x.ai session events, subagent/task
481-
// lifecycle) are parsed centrally in describeUpdate — one parser owns
482-
// all notification shapes.
478+
// Everything else (x.ai session events, subagent/task lifecycle) is
479+
// parsed centrally in describeUpdate — one parser owns all shapes.
480+
// Lifecycle notifications resolve via their inner update.sessionUpdate,
481+
// so describeUpdate never returns null here; hidden control-plane
482+
// notifications never become cards (same gate as the session/update
483+
// stream).
483484
const desc = describeUpdate({
484485
method,
485-
params: params as any,
486+
params,
486487
});
487-
if (desc) {
488-
const now = Date.now();
489-
scheduleLive((prev) =>
490-
reduceAgentUpdate(
491-
prev,
492-
{
493-
handleId,
494-
sessionId,
495-
description: desc,
496-
now,
497-
nextId: () => `${now}-${seq.current++}`,
498-
sourceEventId: e.payload.eventId,
499-
},
500-
timelineReducerState,
501-
)
502-
);
488+
if (shouldDropUpdate(desc)) {
503489
return;
504490
}
505-
506-
if (!isLifecycleNotificationMethod(method)) return;
507-
const desc2 = describeLifecycleNotification(method, params);
508-
if (!desc2) return;
509491
const now = Date.now();
510492
scheduleLive((prev) =>
511493
reduceAgentUpdate(
512494
prev,
513495
{
514496
handleId,
515497
sessionId,
516-
description: desc2,
498+
description: desc,
517499
now,
518500
nextId: () => `${now}-${seq.current++}`,
519501
sourceEventId: e.payload.eventId,

src/utils/format.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
} from "./subagentTasks";
1010
import { extractToolMeta, formatToolCardParts } from "./toolTitle";
1111
import { extractEditDiff, isEditToolUpdate } from "./editDiff";
12-
import { describeSessionEvent } from "./sessionEvents";
12+
import {
13+
describeSessionEvent,
14+
isNoScrollbackExtNotification,
15+
} from "./sessionEvents";
1316

1417
export type { ToolCardParts } from "./toolTitle";
1518
export {
@@ -447,6 +450,13 @@ export function describeUpdate(update: unknown): {
447450
}
448451
default: {
449452
const type = (u.type as string) || sessionUpdate;
453+
// Control-plane x.ai extension notifications (models/settings/MCP/
454+
// announcements sync, prompt acks, …) are consumed host-side or by
455+
// dedicated UI — never Timeline content. Dropped via `hidden`, same as
456+
// Grok Build drops unlisted extension methods.
457+
if (isNoScrollbackExtNotification(method)) {
458+
return { kind: "event", title: type, hidden: true };
459+
}
450460
// Noise phase/events — skip empty ones in the listener
451461
return {
452462
kind: "event",

src/utils/format.turn.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,50 @@ describe("describeUpdate Grok Build scrollback parity", () => {
387387
).toBe(true);
388388
}
389389
});
390+
391+
it("drops control-plane x.ai extension notifications", () => {
392+
for (const method of [
393+
// Both transports: `x.ai/…` and `_x.ai/…`.
394+
"_x.ai/mcp/servers_updated",
395+
"_x.ai/models/update",
396+
"_x.ai/settings/update",
397+
"_x.ai/announcements/update",
398+
"_x.ai/mcp_initialized",
399+
"x.ai/mcp/init_progress",
400+
"x.ai/mcp/tools_changed",
401+
"x.ai/mcp/servers_updated",
402+
"x.ai/sessions/changed",
403+
"x.ai/queue/changed",
404+
"x.ai/git_head_changed",
405+
"x.ai/session/prompt_complete",
406+
"x.ai/scheduled_task_created",
407+
"x.ai/follow_ups",
408+
"x.ai/monitor_event",
409+
]) {
410+
expect(
411+
describeUpdate({ method, params: {} }).hidden,
412+
`${method} must be hidden`,
413+
).toBe(true);
414+
}
415+
});
416+
417+
it("keeps rendered extension notifications and non-x.ai unknowns", () => {
418+
// Lifecycle notifications resolve via their inner update.sessionUpdate.
419+
expect(
420+
describeUpdate({
421+
method: "x.ai/session_notification",
422+
params: {
423+
sessionId: "parent-1",
424+
update: {
425+
sessionUpdate: "subagent_spawned",
426+
child_session_id: "child-1",
427+
},
428+
},
429+
}),
430+
).toMatchObject({ kind: "subagent" });
431+
// Unknown methods outside the x.ai namespace keep the legacy fallback.
432+
expect(
433+
describeUpdate({ method: "acme.custom/thing", params: {} }).hidden,
434+
).toBeUndefined();
435+
});
390436
});

src/utils/sessionEvents.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,26 @@ const GROK_NO_SCROLLBACK_XAI_UPDATES: Record<string, true> = {
5353
model_changed: true,
5454
};
5555

56+
/**
57+
* Whether a JSON-RPC method (agent → client extension notification) must never
58+
* become a Timeline card. The render allowlist is implicit: anything the
59+
* Timeline shows carries an inner `update.sessionUpdate` (subagent/task
60+
* lifecycle, pending interactions, session events) that `describeUpdate`
61+
* resolves before its default branch, so any `x.ai/*` method still reaching
62+
* that branch is control-plane state sync (models/settings/MCP/announcements
63+
* catalogs, prompt acks, …) — consumed host-side or dedicated UI, never
64+
* conversation content. Mirrors Grok Build's `handle_ext_notification`
65+
* dispatch table, whose unlisted methods are dropped.
66+
* Accepts both `x.ai/…` and `_x.ai/…` transports (see Rust
67+
* `models::is_models_update_method` for the same prefix duality).
68+
*/
69+
export function isNoScrollbackExtNotification(
70+
method: string | undefined,
71+
): boolean {
72+
if (!method) return false;
73+
return method.replace(/^_+/, "").startsWith("x.ai/");
74+
}
75+
5676
/** Timeline description shape for event-kind session updates. */
5777
export interface SessionEventDescription {
5878
kind: "event";

0 commit comments

Comments
 (0)