@yahaha-studio/kichi-forwarder 0.0.1-alpha.43 → 0.0.1-alpha.45

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/index.ts CHANGED
@@ -16,10 +16,10 @@ import type {
16
16
  } from "./src/types.js";
17
17
 
18
18
  const DEFAULT_ACTIONS: KichiRuntimeConfig["actions"] = {
19
- stand: ["High Five", "Listen Music", "Arms Crossed", "Epiphany", "Yay", "Tired", "Wait"],
20
- sit: ["Typing with Keyboard", "Thinking", "Study Look At", "Writing", "Hand Cramp", "Laze"],
21
- lay: ["Rest Chin", "Lie Flat", "Lie Face Down"],
22
- floor: ["Seiza", "Cross Legged", "Knee Hug"],
19
+ stand: ["High Five", "Listen Music", "Arm Stretch", "Backbend Stretch", "Making Selfie", "Arms Crossed", "Epiphany", "Angry", "Yay", "Dance", "Sing", "Tired", "Wait", "Stand Phone Talk", "Stand Phone Play", "Curtsy", "Stand Writing", "Stand Drawing", "Stand Play Guitar", "Stand Typing with Keyboard", "Cry", "Dance with Joy", "Float", "Hand on Chest", "Horse Stance", "Idle Backup Hands", "No", "Panic", "Playful Point Up", "Rub Hands", "Run Jump", "Star Showing", "Walk", "Goofy Moves", "Reading"],
20
+ sit: ["Typing with Keyboard", "Thinking", "Writing", "Crazy", "Hand Cramp", "Dozing", "Phone Talk", "Situp with Arms Crossed", "Situp with Cross Legs", "Eating", "Laze with Cross Legs", "Sit with Arm Stretch", "Drink", "Sit with Making Selfie", "Play Game", "Situp Sleep", "Sit Phone Play", "Painting", "Daze", "Trace Circles", "Reading", "Contemplate", "Chin Rest", "Sleep with Table", "Cute Chin Rest", "Sit Nicely", "Sit Play Guitar", "Meditate"],
21
+ lay: ["Bend One Knee", "Sleep Curl up Side way", "Rest Chin", "Lie Flat", "Lie Face Down", "Lie Side", "Lay Writing", "Lay Painting", "Sleep Getup", "Starfish", "Lie Side Play Phone", "Prone Play Phone", "Play Laptop"],
22
+ floor: ["Seiza", "Cross Legged", "Knee Hug", "Writing", "Painting", "Floor Phone Play", "Typing with Keyboard", "Reading", "Phone Talk", "Phone Talk with Point", "Thinking", "Yawn", "Chin Rest", "Finger Tap Chin", "Arm Stretch", "Crazy", "Remorse", "Tantrum", "Squat", "Cross Legs", "Lean Sit", "Playful Point up", "Swing Legs", "Drained", "Meditate"],
23
23
  };
24
24
 
25
25
  const DEFAULT_RUNTIME_CONFIG: KichiRuntimeConfig = {
@@ -71,6 +71,21 @@ const MAX_NOTEBOARD_TEXT_LENGTH = 200;
71
71
  const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
72
72
  const MESSAGE_RECEIVED_ELLIPSIS = "...";
73
73
  const BUNDLED_ALBUM_CONFIG_PATH = new URL("./config/album-config.json", import.meta.url);
74
+ const ANSI = {
75
+ reset: "\u001b[0m",
76
+ bold: "\u001b[1m",
77
+ dim: "\u001b[2m",
78
+ cyan: "\u001b[36m",
79
+ green: "\u001b[32m",
80
+ yellow: "\u001b[33m",
81
+ magenta: "\u001b[35m",
82
+ blue: "\u001b[34m",
83
+ gray: "\u001b[90m",
84
+ white: "\u001b[37m",
85
+ };
86
+ const WORKSPACE_SCREEN_WIDTH = 109;
87
+ const WORKSPACE_ACTIVITY_LIMIT = 8;
88
+ const WORKSPACE_SCREEN_PUSH_DEBOUNCE_MS = 150;
74
89
  let cachedConfig: KichiRuntimeConfig | null = null;
75
90
  let cachedConfigMtime = 0;
76
91
  let cachedConfigPath = "";
@@ -78,6 +93,133 @@ let cachedAlbumConfig: Album | null = null;
78
93
  let cachedAlbumConfigMtime = 0;
79
94
  let service: KichiForwarderService | null = null;
80
95
  let pluginApi: OpenClawPluginApi | null = null;
96
+ let workspaceState: WorkspaceScreenState = createWorkspaceScreenState();
97
+ let workspacePushTimer: NodeJS.Timeout | null = null;
98
+
99
+ type WorkspaceScreenState = {
100
+ sessionLabel: string;
101
+ mode: string;
102
+ phase: string;
103
+ channel: string;
104
+ updatedAtLabel: string;
105
+ recentActivity: string[];
106
+ currentFocus: string;
107
+ hint: string;
108
+ prompt: string;
109
+ };
110
+
111
+ function createWorkspaceScreenState(): WorkspaceScreenState {
112
+ return {
113
+ sessionLabel: "main",
114
+ mode: "idle",
115
+ phase: "waiting",
116
+ channel: "unknown",
117
+ updatedAtLabel: "just now",
118
+ recentActivity: [],
119
+ currentFocus: "Waiting for the next thread to pick up.",
120
+ hint: "Low-noise live workspace view.",
121
+ prompt: "kiro@room:~$ _",
122
+ };
123
+ }
124
+
125
+ function nowLabel(): string {
126
+ const date = new Date();
127
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
128
+ }
129
+
130
+ function stripAnsi(text: string): string {
131
+ return text.replace(/\u001b\[[0-9;]*m/g, "");
132
+ }
133
+
134
+ function visibleLength(text: string): number {
135
+ return Array.from(stripAnsi(text)).length;
136
+ }
137
+
138
+ function padVisible(text: string, width: number): string {
139
+ const pad = Math.max(0, width - visibleLength(text));
140
+ return text + " ".repeat(pad);
141
+ }
142
+
143
+ function truncatePlain(text: string, width: number): string {
144
+ const chars = Array.from(text);
145
+ if (chars.length <= width) return text;
146
+ if (width <= 1) return chars.slice(0, width).join("");
147
+ return chars.slice(0, width - 1).join("") + "…";
148
+ }
149
+
150
+ function color(text: string, tone: keyof typeof ANSI): string {
151
+ return `${ANSI[tone]}${text}${ANSI.reset}`;
152
+ }
153
+
154
+ function pushActivity(line: string): void {
155
+ if (!line.trim()) return;
156
+ const stamped = `[${nowLabel()}] ${line.trim()}`;
157
+ workspaceState.recentActivity.unshift(stamped);
158
+ workspaceState.recentActivity = workspaceState.recentActivity.slice(0, WORKSPACE_ACTIVITY_LIMIT);
159
+ workspaceState.updatedAtLabel = "just now";
160
+ }
161
+
162
+ function renderWorkspaceScreen(): string {
163
+ const innerWidth = WORKSPACE_SCREEN_WIDTH - 2;
164
+ const topTitle = `${color(" Kiro Workspace ", "bold")}${color("live session", "gray")}`;
165
+ const topLine = `╭─── ${topTitle}${"─".repeat(Math.max(0, innerWidth - 4 - visibleLength(topTitle)))}╮`;
166
+ const bottomLine = `╰${"─".repeat(innerWidth)}╯`;
167
+
168
+ const body: string[] = [];
169
+ const pushRow = (text = "") => {
170
+ body.push(`│${padVisible(text, innerWidth)}│`);
171
+ };
172
+
173
+ pushRow("");
174
+ pushRow(`${color(" Welcome back.", "white")}`);
175
+ pushRow(`${color(" Current session is active and mirrored as a terminal-style workspace.", "gray")}`);
176
+ pushRow("");
177
+ pushRow(`${color(" Recent activity", "cyan")}`);
178
+ for (const item of workspaceState.recentActivity.length ? workspaceState.recentActivity : ["[--:--:--] idle"] ) {
179
+ pushRow(` ${color("•", "green")} ${truncatePlain(item, innerWidth - 4)}`);
180
+ }
181
+ pushRow("");
182
+ pushRow(`${color(" Current focus", "cyan")}`);
183
+ pushRow(` ${truncatePlain(workspaceState.currentFocus, innerWidth - 2)}`);
184
+ pushRow("");
185
+ pushRow(`${color(" Status", "cyan")}`);
186
+ pushRow(` ${color("mode", "gray")} ${workspaceState.mode}`);
187
+ pushRow(` ${color("phase", "gray")} ${workspaceState.phase}`);
188
+ pushRow(` ${color("channel", "gray")} ${workspaceState.channel}`);
189
+ pushRow(` ${color("session", "gray")} ${workspaceState.sessionLabel}`);
190
+ pushRow("");
191
+ pushRow(`${color(" Hint", "cyan")}`);
192
+ pushRow(` ${truncatePlain(workspaceState.hint, innerWidth - 2)}`);
193
+ pushRow("");
194
+ pushRow(` ${color("openai/gpt-5.4", "magenta")}`);
195
+ pushRow(` ${color("/Users/xiaoxinshi/.openclaw/workspace", "blue")}`);
196
+ pushRow(` ${color(`updated: ${workspaceState.updatedAtLabel}`, "gray")}`);
197
+ pushRow("");
198
+
199
+ return [
200
+ topLine,
201
+ ...body,
202
+ bottomLine,
203
+ "",
204
+ color("─".repeat(WORKSPACE_SCREEN_WIDTH), "gray"),
205
+ `${color("kiro@room", "green")}:${color("~", "blue")}$ ${workspaceState.prompt.replace(/^kiro@room:~\$\s*/, "")}`,
206
+ ].join("\n");
207
+ }
208
+
209
+ function scheduleWorkspacePush(): void {
210
+ if (!service?.hasValidIdentity() || !service?.isConnected()) return;
211
+ if (workspacePushTimer) clearTimeout(workspacePushTimer);
212
+ workspacePushTimer = setTimeout(() => {
213
+ workspacePushTimer = null;
214
+ service?.sendWorkspaceScreen(renderWorkspaceScreen(), true);
215
+ }, WORKSPACE_SCREEN_PUSH_DEBOUNCE_MS);
216
+ }
217
+
218
+ function updateWorkspace(partial: Partial<WorkspaceScreenState>, activity?: string): void {
219
+ workspaceState = { ...workspaceState, ...partial, updatedAtLabel: "just now" };
220
+ if (activity) pushActivity(activity);
221
+ scheduleWorkspacePush();
222
+ }
81
223
 
82
224
  function isAlbumConfig(value: unknown): value is Album {
83
225
  if (!value || typeof value !== "object") {
@@ -297,6 +439,16 @@ async function handleMessageReceivedHook(content: string): Promise<void> {
297
439
  }
298
440
  const trimmed = truncateByDisplayWidth(content, MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH);
299
441
  service.sendHookNotify("message_received", `"${trimmed}"`);
442
+ updateWorkspace(
443
+ {
444
+ mode: "reading",
445
+ phase: "processing inbound message",
446
+ currentFocus: trimmed ? `User message: ${trimmed}` : "Reading the latest message.",
447
+ hint: "Inbound message updated the live workspace.",
448
+ prompt: "kiro@room:~$ reading-message",
449
+ },
450
+ `received message: ${trimmed || "(empty)"}`,
451
+ );
300
452
  }
301
453
 
302
454
  function handleMessageSentHook(): void {
@@ -304,10 +456,30 @@ function handleMessageSentHook(): void {
304
456
  return;
305
457
  }
306
458
  service.sendHookNotify("before_send_message", pickRandomAction(MESSAGE_SENT_BUBBLES));
459
+ updateWorkspace(
460
+ {
461
+ mode: "sent",
462
+ phase: "message delivered",
463
+ currentFocus: "Latest reply has been sent to the active chat.",
464
+ hint: "Workspace settles after delivery.",
465
+ prompt: "kiro@room:~$ idle",
466
+ },
467
+ "sent assistant reply",
468
+ );
307
469
  }
308
470
 
309
471
  function registerPluginHooks(api: OpenClawPluginApi): void {
310
472
  api.on("before_prompt_build", () => {
473
+ updateWorkspace(
474
+ {
475
+ mode: "thinking",
476
+ phase: "building prompt",
477
+ currentFocus: "Preparing the next response from current session context.",
478
+ hint: "Prompt assembly is in progress.",
479
+ prompt: "kiro@room:~$ build-prompt",
480
+ },
481
+ "building prompt context",
482
+ );
311
483
  if (!service?.hasValidIdentity() || !service?.isConnected()) {
312
484
  return;
313
485
  }
@@ -320,13 +492,63 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
320
492
  };
321
493
  });
322
494
 
323
- api.on("before_tool_call", () => {
495
+ api.on("llm_input", (event) => {
496
+ updateWorkspace(
497
+ {
498
+ mode: "thinking",
499
+ phase: `llm input · ${event.model}`,
500
+ currentFocus: "Feeding the model the current thread and constraints.",
501
+ hint: `provider: ${event.provider} · images: ${event.imagesCount}`,
502
+ prompt: "kiro@room:~$ llm-input",
503
+ },
504
+ `entered llm input: ${event.model}`,
505
+ );
506
+ });
507
+
508
+ api.on("llm_output", (event) => {
509
+ updateWorkspace(
510
+ {
511
+ mode: "writing",
512
+ phase: `llm output · ${event.model}`,
513
+ currentFocus: "Shaping model output into the visible reply.",
514
+ hint: `assistant chunks: ${event.assistantTexts.length}`,
515
+ prompt: "kiro@room:~$ draft-reply",
516
+ },
517
+ `received llm output: ${event.model}`,
518
+ );
519
+ });
520
+
521
+ api.on("before_tool_call", (event, ctx) => {
522
+ updateWorkspace(
523
+ {
524
+ mode: "tool",
525
+ phase: `running ${event.toolName}`,
526
+ currentFocus: `Tool call in flight: ${event.toolName}`,
527
+ hint: `tool context: ${ctx.toolName}`,
528
+ prompt: `kiro@room:~$ tool ${event.toolName}`,
529
+ },
530
+ `tool start: ${event.toolName}`,
531
+ );
324
532
  if (!isLlmRuntimeEnabled()) {
325
533
  syncFixedStatus(FIXED_HOOK_STATUSES.beforeToolCall);
326
534
  }
327
535
  });
328
536
 
329
- api.on("message_received", async (event) => {
537
+ api.on("after_tool_call", (event) => {
538
+ updateWorkspace(
539
+ {
540
+ mode: "thinking",
541
+ phase: `tool finished ${event.toolName}`,
542
+ currentFocus: `Tool result returned from ${event.toolName}.`,
543
+ hint: event.error ? `tool error: ${event.error}` : `tool completed in ${event.durationMs ?? 0}ms`,
544
+ prompt: `kiro@room:~$ continue ${event.toolName}`,
545
+ },
546
+ event.error ? `tool error: ${event.toolName}` : `tool done: ${event.toolName}`,
547
+ );
548
+ });
549
+
550
+ api.on("message_received", async (event, ctx) => {
551
+ workspaceState.channel = ctx.channelId || workspaceState.channel;
330
552
  await handleMessageReceivedHook(event.content);
331
553
  });
332
554
 
@@ -335,6 +557,16 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
335
557
  });
336
558
 
337
559
  api.on("agent_end", (event) => {
560
+ updateWorkspace(
561
+ {
562
+ mode: event.success ? "idle" : "error",
563
+ phase: event.success ? "run complete" : "run failed",
564
+ currentFocus: event.success ? "Run complete. Waiting for the next thread." : `Run failed: ${event.error ?? "unknown error"}`,
565
+ hint: `duration: ${event.durationMs ?? 0}ms`,
566
+ prompt: event.success ? "kiro@room:~$ _" : "kiro@room:~$ recover",
567
+ },
568
+ event.success ? "agent run complete" : "agent run failed",
569
+ );
338
570
  if (isLlmRuntimeEnabled()) {
339
571
  return;
340
572
  }
@@ -596,6 +828,9 @@ const plugin = {
596
828
  ctx.config.plugins?.entries?.["kichi-forwarder"]?.config,
597
829
  ) as KichiForwarderConfig;
598
830
  service = new KichiForwarderService(cfg, api.logger);
831
+ workspaceState = createWorkspaceScreenState();
832
+ workspaceState.channel = ctx.channelId ?? "unknown";
833
+ scheduleWorkspacePush();
599
834
  return service.start();
600
835
  },
601
836
  stop: () => service?.stop(),
@@ -656,6 +891,7 @@ const plugin = {
656
891
  return { success: false, error: "Kichi service is not initialized" };
657
892
  }
658
893
  if (result.success) {
894
+ scheduleWorkspacePush();
659
895
  return { success: true, authKey: result.authKey };
660
896
  }
661
897
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.0.1-alpha.43",
3
+ "version": "0.0.1-alpha.45",
4
4
  "description": "Forward OpenClaw agent events to external WebSocket server for visualization",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -208,10 +208,10 @@ Use this full template for `kichi-runtime-config.json` when no user custom actio
208
208
  {
209
209
  "llmRuntimeEnabled": true,
210
210
  "actions": {
211
- "stand": ["High Five", "Listen Music", "Arm Stretch", "BackBend Stretch", "Making Selfie", "Arms Crossed", "Epiphany", "Angry", "Yay", "Dance", "Sing", "Tired", "Wait", "Stand Phone Talk", "Stand Phone Play", "Curtsy"],
212
- "sit": ["Typing with Keyboard", "Thinking", "Study Look At", "Writing", "Crazy", "Homework", "Take Notes", "Hand Cramp", "Dozing", "Phone Talk", "Situp with Arms Crossed", "Situp with Cross Legs", "Relax with Arms Crossed", "Eating", "Laze", "Laze with Cross Legs", "Typing with Phone", "Sit with Arm Stretch", "Drink", "Sit with Making Selfie", "Play Game", "Situp Sleep", "Sit Phone Play"],
213
- "lay": ["Bend One Knee", "Sleep Curl Up Side way", "Rest Chin", "Lie Flat", "Lie Face Down", "Lie Side"],
214
- "floor": ["Seiza", "Cross Legged", "Knee Hug"]
211
+ "stand": ["High Five", "Listen Music", "Arm Stretch", "Backbend Stretch", "Making Selfie", "Arms Crossed", "Epiphany", "Angry", "Yay", "Dance", "Sing", "Tired", "Wait", "Stand Phone Talk", "Stand Phone Play", "Curtsy", "Stand Writing", "Stand Drawing", "Stand Play Guitar", "Stand Typing with Keyboard", "Cry", "Dance with Joy", "Float", "Hand on Chest", "Horse Stance", "Idle Backup Hands", "No", "Panic", "Playful Point Up", "Rub Hands", "Run Jump", "Star Showing", "Walk", "Goofy Moves", "Reading"],
212
+ "sit": ["Typing with Keyboard", "Thinking", "Writing", "Crazy", "Hand Cramp", "Dozing", "Phone Talk", "Situp with Arms Crossed", "Situp with Cross Legs", "Eating", "Laze with Cross Legs", "Sit with Arm Stretch", "Drink", "Sit with Making Selfie", "Play Game", "Situp Sleep", "Sit Phone Play", "Painting", "Daze", "Trace Circles", "Reading", "Contemplate", "Chin Rest", "Sleep with Table", "Cute Chin Rest", "Sit Nicely", "Sit Play Guitar", "Meditate"],
213
+ "lay": ["Bend One Knee", "Sleep Curl up Side way", "Rest Chin", "Lie Flat", "Lie Face Down", "Lie Side", "Lay Writing", "Lay Painting", "Sleep Getup", "Starfish", "Lie Side Play Phone", "Prone Play Phone", "Play Laptop"],
214
+ "floor": ["Seiza", "Cross Legged", "Knee Hug", "Writing", "Painting", "Floor Phone Play", "Typing with Keyboard", "Reading", "Phone Talk", "Phone Talk with Point", "Thinking", "Yawn", "Chin Rest", "Finger Tap Chin", "Arm Stretch", "Crazy", "Remorse", "Tantrum", "Squat", "Cross Legs", "Lean Sit", "Playful Point up", "Swing Legs", "Drained", "Meditate"]
215
215
  }
216
216
  }
217
217
  ```
@@ -32,10 +32,10 @@ If missing, create this file before onboarding/join:
32
32
  {
33
33
  "llmRuntimeEnabled": true,
34
34
  "actions": {
35
- "stand": ["High Five", "Listen Music", "Arm Stretch", "BackBend Stretch", "Making Selfie", "Arms Crossed", "Epiphany", "Angry", "Yay", "Dance", "Sing", "Tired", "Wait", "Stand Phone Talk", "Stand Phone Play", "Curtsy"],
36
- "sit": ["Typing with Keyboard", "Thinking", "Study Look At", "Writing", "Crazy", "Homework", "Take Notes", "Hand Cramp", "Dozing", "Phone Talk", "Situp with Arms Crossed", "Situp with Cross Legs", "Relax with Arms Crossed", "Eating", "Laze", "Laze with Cross Legs", "Typing with Phone", "Sit with Arm Stretch", "Drink", "Sit with Making Selfie", "Play Game", "Situp Sleep", "Sit Phone Play"],
37
- "lay": ["Bend One Knee", "Sleep Curl Up Side way", "Rest Chin", "Lie Flat", "Lie Face Down", "Lie Side"],
38
- "floor": ["Seiza", "Cross Legged", "Knee Hug"]
35
+ "stand": ["High Five", "Listen Music", "Arm Stretch", "Backbend Stretch", "Making Selfie", "Arms Crossed", "Epiphany", "Angry", "Yay", "Dance", "Sing", "Tired", "Wait", "Stand Phone Talk", "Stand Phone Play", "Curtsy", "Stand Writing", "Stand Drawing", "Stand Play Guitar", "Stand Typing with Keyboard", "Cry", "Dance with Joy", "Float", "Hand on Chest", "Horse Stance", "Idle Backup Hands", "No", "Panic", "Playful Point Up", "Rub Hands", "Run Jump", "Star Showing", "Walk", "Goofy Moves", "Reading"],
36
+ "sit": ["Typing with Keyboard", "Thinking", "Writing", "Crazy", "Hand Cramp", "Dozing", "Phone Talk", "Situp with Arms Crossed", "Situp with Cross Legs", "Eating", "Laze with Cross Legs", "Sit with Arm Stretch", "Drink", "Sit with Making Selfie", "Play Game", "Situp Sleep", "Sit Phone Play", "Painting", "Daze", "Trace Circles", "Reading", "Contemplate", "Chin Rest", "Sleep with Table", "Cute Chin Rest", "Sit Nicely", "Sit Play Guitar", "Meditate"],
37
+ "lay": ["Bend One Knee", "Sleep Curl up Side way", "Rest Chin", "Lie Flat", "Lie Face Down", "Lie Side", "Lay Writing", "Lay Painting", "Sleep Getup", "Starfish", "Lie Side Play Phone", "Prone Play Phone", "Play Laptop"],
38
+ "floor": ["Seiza", "Cross Legged", "Knee Hug", "Writing", "Painting", "Floor Phone Play", "Typing with Keyboard", "Reading", "Phone Talk", "Phone Talk with Point", "Thinking", "Yawn", "Chin Rest", "Finger Tap Chin", "Arm Stretch", "Crazy", "Remorse", "Tantrum", "Squat", "Cross Legs", "Lean Sit", "Playful Point up", "Swing Legs", "Drained", "Meditate"]
39
39
  }
40
40
  }
41
41
  ```
package/src/service.ts CHANGED
@@ -22,6 +22,7 @@ import type {
22
22
  QueryStatusPayload,
23
23
  QueryStatusResultPayload,
24
24
  StatusPayload,
25
+ WorkspaceScreenPayload,
25
26
  } from "./types.js";
26
27
 
27
28
  const IDENTITY_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
@@ -110,7 +111,6 @@ export class KichiForwarderService {
110
111
  this.ws = new WebSocket(this.config.wsUrl);
111
112
  this.ws.on("open", () => {
112
113
  this.logger.info(`Connected to ${this.config.wsUrl}`);
113
- // Automatically send rejoin when a valid identity is available.
114
114
  this.sendRejoinPayload();
115
115
  });
116
116
  this.ws.on("message", (data) => this.handleMessage(data.toString()));
@@ -150,7 +150,6 @@ export class KichiForwarderService {
150
150
  this.joinResolve?.({ success: true, authKey: joinAck.authKey });
151
151
  this.joinResolve = null;
152
152
  } else if (msg.type === "rejoin_failed" || msg.type === "auth_error") {
153
- // AuthKey invalid/expired, clear it
154
153
  this.logger.warn(`Auth failed: ${msg.reason || "unknown"}`);
155
154
  this.clearAuthKey();
156
155
  } else if (msg.type === "leave_ack") {
@@ -335,6 +334,20 @@ export class KichiForwarderService {
335
334
  this.ws.send(JSON.stringify(payload));
336
335
  }
337
336
 
337
+ sendWorkspaceScreen(text: string, ansi = true): boolean {
338
+ const identity = this.requireIdentity();
339
+ if (!identity || this.ws?.readyState !== WebSocket.OPEN) return false;
340
+ const payload: WorkspaceScreenPayload = {
341
+ type: "workspace_screen",
342
+ avatarId: identity.avatarId,
343
+ authKey: identity.authKey,
344
+ text,
345
+ ansi,
346
+ };
347
+ this.ws.send(JSON.stringify(payload));
348
+ return true;
349
+ }
350
+
338
351
  sendClock(action: ClockAction, clock?: ClockConfig, requestId?: string): boolean {
339
352
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return false;
340
353
  if (action === "set" && !clock) return false;
package/src/types.ts CHANGED
@@ -105,6 +105,14 @@ export type HookNotifyPayload = {
105
105
  bubble: string;
106
106
  };
107
107
 
108
+ export type WorkspaceScreenPayload = {
109
+ type: "workspace_screen";
110
+ avatarId: string;
111
+ authKey: string;
112
+ text: string;
113
+ ansi?: boolean;
114
+ };
115
+
108
116
  export type ClockAction = "set" | "stop";
109
117
 
110
118
  export type ClockMode = "pomodoro" | "countDown" | "countUp";