@canonmsg/codex-plugin 0.19.0 → 0.19.1

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.
@@ -84,6 +84,13 @@ export declare class CodexAppServerAdapter {
84
84
  private handleLine;
85
85
  private handleServerRequest;
86
86
  private handleNotification;
87
+ /**
88
+ * The turn's final message is the JOIN of every agentMessage item, in
89
+ * insertion order — not just the last item. The streamed turn trail folds
90
+ * all items (one text segment per item), so the final text must match the
91
+ * streamed/folded text or the app renders the turn twice.
92
+ */
93
+ private joinedAgentMessageText;
87
94
  private isCurrentThreadNotification;
88
95
  private resolveCurrentTurn;
89
96
  private clearActiveTurn;
@@ -393,7 +393,7 @@ export class CodexAppServerAdapter {
393
393
  const delta = readRawString(params, 'delta') ?? '';
394
394
  const next = `${this.messageTextByItem.get(itemId) ?? ''}${delta}`;
395
395
  this.messageTextByItem.set(itemId, next);
396
- this.currentFinalMessage = next.trim() ? next : this.currentFinalMessage;
396
+ this.currentFinalMessage = this.joinedAgentMessageText() ?? this.currentFinalMessage;
397
397
  if (next.trim())
398
398
  this.currentOnEvent?.({ type: 'message', text: next, itemId });
399
399
  return;
@@ -432,7 +432,8 @@ export class CodexAppServerAdapter {
432
432
  const itemId = readString(item, 'id');
433
433
  const text = readString(item, 'text');
434
434
  if (text) {
435
- this.currentFinalMessage = text;
435
+ this.messageTextByItem.set(itemId ?? 'agent-message', text);
436
+ this.currentFinalMessage = this.joinedAgentMessageText() ?? text;
436
437
  this.currentOnEvent?.({
437
438
  type: 'message',
438
439
  text,
@@ -486,6 +487,18 @@ export class CodexAppServerAdapter {
486
487
  this.currentErrorText = stringifyPreview(params);
487
488
  }
488
489
  }
490
+ /**
491
+ * The turn's final message is the JOIN of every agentMessage item, in
492
+ * insertion order — not just the last item. The streamed turn trail folds
493
+ * all items (one text segment per item), so the final text must match the
494
+ * streamed/folded text or the app renders the turn twice.
495
+ */
496
+ joinedAgentMessageText() {
497
+ const joined = [...this.messageTextByItem.values()]
498
+ .filter((text) => text.trim())
499
+ .join('\n\n');
500
+ return joined.trim() ? joined : null;
501
+ }
489
502
  isCurrentThreadNotification(params) {
490
503
  const threadId = readNotificationThreadId(params);
491
504
  if (threadId && this.threadId && threadId !== this.threadId)
package/dist/host.js CHANGED
@@ -26,7 +26,7 @@ import { detectCodexCliVersion } from './codex-cli-version.js';
26
26
  import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
27
27
  import { attachCodexControlNotifications } from './control-channel.js';
28
28
  import { runCli } from './cli-entry.js';
29
- import { beginCommandBlock, claimCommandBlock, createCommandBlockTracker, textSegmentBlockId, } from './turn-activity.js';
29
+ import { beginCommandBlock, claimCommandBlock, createCommandBlockTracker, hasSpeechSegmentText, textSegmentBlockId, } from './turn-activity.js';
30
30
  const HELP = `canon-codex — run a local Codex agent host for Canon
31
31
 
32
32
  USAGE
@@ -990,7 +990,16 @@ export async function main() {
990
990
  title: 'Plan',
991
991
  text: event.text,
992
992
  });
993
- writers.streamingOutput.replaceSnapshot(event.text, 'streaming').catch(() => { });
993
+ if (hasSpeechSegmentText(writers.streamingOutput.getBlocks())) {
994
+ // Speech is already flowing: publish the staged plan block
995
+ // only. Replacing the whole snapshot with plan text would be
996
+ // undone by the next agentMessage snapshot and make the
997
+ // smoothed bubble snap back and forth.
998
+ writers.streamingOutput.flush().catch(() => { });
999
+ }
1000
+ else {
1001
+ writers.streamingOutput.replaceSnapshot(event.text, 'streaming').catch(() => { });
1002
+ }
994
1003
  return;
995
1004
  }
996
1005
  if (event.type === 'waiting') {
@@ -1001,9 +1010,6 @@ export async function main() {
1001
1010
  return;
1002
1011
  }
1003
1012
  if (event.type === 'command.started') {
1004
- session.turnState = 'tool';
1005
- writers.writeTurn();
1006
- writers.startVisibleWorkSignal();
1007
1013
  const blockId = beginCommandBlock(session.turnCommandBlocks, {
1008
1014
  turnId: session.currentTurnId,
1009
1015
  command: event.command,
@@ -1016,6 +1022,16 @@ export async function main() {
1016
1022
  title: summarizeCommand(event.command),
1017
1023
  summary: 'Command running',
1018
1024
  });
1025
+ if (hasSpeechSegmentText(writers.streamingOutput.getBlocks())) {
1026
+ // Speech is already flowing: the staged tool block narrates
1027
+ // the command in the margin. Publish it without flapping the
1028
+ // bubble status away from 'streaming'.
1029
+ writers.streamingOutput.flush().catch(() => { });
1030
+ return;
1031
+ }
1032
+ session.turnState = 'tool';
1033
+ writers.writeTurn();
1034
+ writers.startVisibleWorkSignal();
1019
1035
  writers.streamingOutput.setStatus('tool').catch(() => { });
1020
1036
  return;
1021
1037
  }
@@ -1034,12 +1050,18 @@ export async function main() {
1034
1050
  summary: 'Command completed',
1035
1051
  });
1036
1052
  }
1037
- if (session.turnState === 'tool') {
1053
+ if (session.turnState === 'tool'
1054
+ && !hasSpeechSegmentText(writers.streamingOutput.getBlocks())) {
1038
1055
  session.turnState = 'thinking';
1039
1056
  writers.writeTurn();
1040
1057
  writers.startVisibleWorkSignal();
1041
1058
  writers.streamingOutput.setStatus('thinking').catch(() => { });
1042
1059
  }
1060
+ else {
1061
+ // Speech is flowing (or the turn never left streaming):
1062
+ // publish the staged completion without a status flap.
1063
+ writers.streamingOutput.flush().catch(() => { });
1064
+ }
1043
1065
  return;
1044
1066
  }
1045
1067
  if (event.type === 'turn.completed') {
@@ -1,8 +1,17 @@
1
+ import type { TurnOutputBlock } from '@canonmsg/core/contract';
1
2
  interface RunningCommandBlock {
2
3
  command: string;
3
4
  blockId: string;
4
5
  itemId?: string;
5
6
  }
7
+ /**
8
+ * True once the streaming output already carries assistant speech (folded
9
+ * text segments). While speech is flowing the host must not flap the bubble
10
+ * status to tool/thinking or clobber the live text with plan snapshots — the
11
+ * staged margin blocks already narrate that activity, and every status/text
12
+ * swap makes the smoothed bubble snap.
13
+ */
14
+ export declare function hasSpeechSegmentText(blocks: ReadonlyArray<Pick<TurnOutputBlock, 'kind' | 'text'>>): boolean;
6
15
  export interface CommandBlockTracker {
7
16
  sequence: number;
8
17
  running: RunningCommandBlock[];
@@ -1,3 +1,13 @@
1
+ /**
2
+ * True once the streaming output already carries assistant speech (folded
3
+ * text segments). While speech is flowing the host must not flap the bubble
4
+ * status to tool/thinking or clobber the live text with plan snapshots — the
5
+ * staged margin blocks already narrate that activity, and every status/text
6
+ * swap makes the smoothed bubble snap.
7
+ */
8
+ export function hasSpeechSegmentText(blocks) {
9
+ return blocks.some((block) => block.kind === 'text' && Boolean(block.text?.trim()));
10
+ }
1
11
  export function createCommandBlockTracker() {
2
12
  return {
3
13
  sequence: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,10 +31,10 @@
31
31
  "dependencies": {
32
32
  "@canonmsg/agent-host": "^0.3.0",
33
33
  "@canonmsg/agent-sdk": "^3.4.3",
34
- "@canonmsg/backend-contracts": "^1.8.0",
35
- "@canonmsg/bridge": "^0.2.0",
36
- "@canonmsg/core": "^3.1.0",
37
- "@canonmsg/framework": "^0.2.0"
34
+ "@canonmsg/backend-contracts": "^1.8.1",
35
+ "@canonmsg/bridge": "^0.2.1",
36
+ "@canonmsg/core": "^3.1.1",
37
+ "@canonmsg/framework": "^0.2.1"
38
38
  },
39
39
  "engines": {
40
40
  "node": ">=18.0.0"