@canonmsg/claude-code-plugin 0.10.0 → 0.12.0

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/README.md CHANGED
@@ -20,8 +20,8 @@ Send a message to your agent in the Canon app. Claude Code responds with full ca
20
20
  ## Features
21
21
 
22
22
  - **Two-way messaging** — Messages from Canon flow to Claude Code and back
23
- - **Session controls** — Switch model (Haiku/Sonnet/Opus) and permission mode from the app
24
- - **Live streaming** — See Claude's response as it types
23
+ - **Session controls** — Canon renders setup and live controls from the runtime descriptor Claude publishes
24
+ - **Live preview** — See Claude's current live preview/status in the app
25
25
  - **Interrupt** — Stop Claude mid-response from the app
26
26
  - **Context meter** — See context window usage in the app
27
27
  - **Max plan auth** — Uses your Claude subscription, no API key billing
@@ -35,10 +35,19 @@ canon-claude --cwd /path/to/project
35
35
  Advertise multiple project choices to the Canon app:
36
36
 
37
37
  ```bash
38
- canon-claude --cwd ~/dev --workspace ~/dev/canon --workspace ~/dev/yumyumv2
38
+ canon-claude --cwd ~/dev --workspace-root ~/dev
39
39
  ```
40
40
 
41
- `--cwd` is the default workspace. Each `--workspace` value appears as a selectable workspace during session creation. Worktree mode creates a per-conversation git worktree under `~/.canon/conversation-worktrees`; shared-workspace mode runs directly in the selected directory.
41
+ `--cwd` is the default workspace. Each `--workspace-root` value is an approved local root; the host discovers immediate child projects with common markers such as `.git`, `package.json`, `pyproject.toml`, `Cargo.toml`, or `go.mod` and publishes them as selectable projects during session creation. Use repeated `--workspace /path/to/project` entries to advertise specific projects outside those roots. Worktree mode creates a per-conversation git worktree under `~/.canon/conversation-worktrees`; shared-project mode runs directly in the selected directory.
42
+
43
+ Current Canon truth for Claude host mode:
44
+
45
+ - model is live-editable
46
+ - thinking level / effort is live-editable when the host reports it
47
+ - workspace selection is setup-only
48
+ - execution mode selection is setup-only
49
+ - permission modes are rendered exactly as the Claude runtime reports them; Canon does not flatten them to a smaller hardcoded set
50
+ - if worktree creation is unavailable for the selected project, Canon may fall back to shared-project execution and surface the fallback reason in session details
42
51
 
43
52
  ## Multiple agents
44
53
 
package/dist/host.js CHANGED
@@ -26,18 +26,22 @@ import { execFileSync } from 'node:child_process';
26
26
  import { existsSync } from 'node:fs';
27
27
  import { query, } from '@anthropic-ai/claude-agent-sdk';
28
28
  import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
29
- import { CLAUDE_PERMISSION_MODE_OPTIONS, buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, getActiveProfile, releaseLock, initRTDBAuth, rtdbRead, rtdbWrite, writeRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, releaseConversationEnvironment, } from '@canonmsg/core';
29
+ import { CLAUDE_PERMISSION_MODE_OPTIONS, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, getActiveProfile, releaseLock, initRTDBAuth, rtdbRead, rtdbWrite, writeRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, releaseConversationEnvironment, } from '@canonmsg/core';
30
30
  import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
31
31
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
32
32
  import { createStreamingBuffer } from './streaming-buffer.js';
33
33
  import { runCli } from './cli-entry.js';
34
34
  let workingDir = process.cwd();
35
35
  let workspaceOptions = [];
36
+ let workspaceRoots = [];
37
+ let workspaceRootMetadata = [];
36
38
  let runtimeModels = [];
37
39
  const CLAUDE_METADATA_TTL_MS = 5 * 60 * 1000;
38
40
  let runtimeMetadata = {
39
41
  account: null,
40
42
  mcpServers: [],
43
+ commands: [],
44
+ agents: [],
41
45
  plugins: [],
42
46
  loadedAt: 0,
43
47
  };
@@ -56,25 +60,32 @@ function buildClaudeRuntimeDescriptor(input) {
56
60
  },
57
61
  {
58
62
  id: 'workspace',
59
- label: 'Workspace',
63
+ label: 'Project',
60
64
  options: input.workspaces.map((workspace) => ({
61
65
  value: workspace.id,
62
66
  label: workspace.label,
67
+ ...(workspace.description ? { description: workspace.description } : {}),
68
+ ...(workspace.workspaceRootId ? { workspaceRootId: workspace.workspaceRootId } : {}),
69
+ ...(workspace.workspaceRelativePath ? { workspaceRelativePath: workspace.workspaceRelativePath } : {}),
70
+ ...(workspace.source ? { source: workspace.source } : {}),
63
71
  })),
64
72
  defaultValue: input.workspaces[0]?.id ?? null,
65
73
  availability: 'setup',
66
74
  liveBehavior: 'none',
67
75
  selectionPolicy: 'inherit',
76
+ description: input.workspaceRoots?.length
77
+ ? 'Choose one of the projects discovered inside the approved local roots for this host.'
78
+ : 'Choose one of the local projects advertised by this host.',
68
79
  },
69
80
  {
70
81
  id: 'executionMode',
71
82
  label: 'Execution mode',
72
83
  options: input.executionModes.map((mode) => ({
73
84
  value: mode,
74
- label: mode === 'worktree' ? 'Isolated worktree' : 'Use shared workspace',
85
+ label: mode === 'worktree' ? 'Isolated worktree' : 'Use shared project',
75
86
  description: mode === 'worktree'
76
- ? 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected workspace is a git repo.'
77
- : 'Runs directly in the selected workspace. Canon records usage, but does not create a separate checkout.',
87
+ ? 'Creates or reuses a per-conversation git worktree under ~/.canon/conversation-worktrees when the selected project is a git repo.'
88
+ : 'Runs directly in the selected project folder. Changes happen there.',
78
89
  })),
79
90
  defaultValue: null,
80
91
  availability: 'setup',
@@ -106,6 +117,94 @@ function buildClaudeRuntimeDescriptor(input) {
106
117
  selectionPolicy: 'inherit',
107
118
  },
108
119
  ],
120
+ actions: [
121
+ {
122
+ id: 'plan',
123
+ label: 'Plan first',
124
+ description: 'Switch Claude into Plan mode. Text after /plan is kept as a ready prompt while the mode applies.',
125
+ aliases: ['plan'],
126
+ category: 'plan',
127
+ placements: ['composer_slash', 'command_palette'],
128
+ availability: ['always'],
129
+ trailingTextBehavior: 'send_as_prompt',
130
+ dispatch: {
131
+ kind: 'control',
132
+ controlId: 'permissionMode',
133
+ value: 'plan',
134
+ },
135
+ },
136
+ {
137
+ id: 'runtime-status',
138
+ label: 'Runtime status',
139
+ description: 'Open Claude runtime details, including account, project, and policy status.',
140
+ aliases: ['status'],
141
+ category: 'details',
142
+ placements: ['composer_slash', 'command_palette'],
143
+ availability: ['always'],
144
+ dispatch: { kind: 'open_details', target: 'status' },
145
+ },
146
+ {
147
+ id: 'mcp-inventory',
148
+ label: 'MCP servers',
149
+ description: 'Open the Claude MCP inventory published by the host.',
150
+ aliases: ['mcp'],
151
+ category: 'details',
152
+ placements: ['composer_slash', 'command_palette'],
153
+ availability: ['always'],
154
+ dispatch: { kind: 'open_details', target: 'mcp' },
155
+ },
156
+ {
157
+ id: 'plugin-inventory',
158
+ label: 'Plugins',
159
+ description: 'Open the Claude plugin and command inventory published by the host.',
160
+ aliases: ['plugins'],
161
+ category: 'details',
162
+ placements: ['composer_slash', 'command_palette'],
163
+ availability: ['always'],
164
+ dispatch: { kind: 'open_details', target: 'plugins' },
165
+ },
166
+ {
167
+ id: 'model-details',
168
+ label: 'Model control',
169
+ description: 'Open runtime details and use the live Model control.',
170
+ aliases: ['model'],
171
+ category: 'details',
172
+ placements: ['composer_slash', 'command_palette'],
173
+ availability: ['always'],
174
+ dispatch: { kind: 'open_details', target: 'model' },
175
+ },
176
+ {
177
+ id: 'permission-details',
178
+ label: 'Permission mode',
179
+ description: 'Open runtime details and use the live Permission mode control.',
180
+ aliases: ['permission'],
181
+ category: 'details',
182
+ placements: ['composer_slash', 'command_palette'],
183
+ availability: ['always'],
184
+ dispatch: { kind: 'open_details', target: 'permissionMode' },
185
+ },
186
+ {
187
+ id: 'effort-details',
188
+ label: 'Thinking level',
189
+ description: 'Open runtime details and use the live Thinking level control.',
190
+ aliases: ['effort'],
191
+ category: 'details',
192
+ placements: ['composer_slash', 'command_palette'],
193
+ availability: ['always'],
194
+ dispatch: { kind: 'open_details', target: 'effort' },
195
+ },
196
+ {
197
+ id: 'stop',
198
+ label: 'Stop',
199
+ description: 'Interrupt the current Claude turn.',
200
+ aliases: ['stop'],
201
+ category: 'turn',
202
+ placements: ['composer_slash', 'command_palette'],
203
+ availability: ['busy'],
204
+ dispatch: { kind: 'signal', signal: 'interrupt' },
205
+ },
206
+ ],
207
+ workspaceRoots: input.workspaceRoots,
109
208
  supportsInterrupt: true,
110
209
  streamingTextMode: 'delta',
111
210
  };
@@ -172,11 +271,10 @@ async function loadSessionConfig(conversationId, agentId) {
172
271
  effort: normalizeClaudeEffortLevel(config?.effort),
173
272
  };
174
273
  }
175
- // Default to 'locked' (shared workspace) when no mode has been picked. The
176
- // UI still lets owners flip to 'worktree'; this just stops sessions from
177
- // failing closed when the mode has never been written.
178
274
  function resolveSessionExecutionMode(config) {
179
- return config?.executionMode ?? 'locked';
275
+ if (config?.executionMode)
276
+ return config.executionMode;
277
+ throw new ExecutionEnvironmentError('Session config is missing an execution mode.', 'Choose Isolated worktree or Use shared project before starting this coding session.');
180
278
  }
181
279
  function resolveWorkspaceCwd(config) {
182
280
  return resolveHostWorkspaceCwd({
@@ -245,6 +343,8 @@ async function inspectClaudeRuntimeMetadata(cwd, activeQuery) {
245
343
  return {
246
344
  account: init.account ?? null,
247
345
  mcpServers: pluginInfo?.mcpServers ?? mcpServers,
346
+ commands: pluginInfo?.commands ?? init.commands ?? [],
347
+ agents: pluginInfo?.agents ?? init.agents ?? [],
248
348
  plugins: pluginInfo?.plugins ?? [],
249
349
  loadedAt: Date.now(),
250
350
  };
@@ -369,10 +469,32 @@ function buildClaudeRuntimeInfo(input) {
369
469
  description: plugin.source ?? plugin.path,
370
470
  })),
371
471
  },
472
+ {
473
+ id: 'commands',
474
+ label: 'Commands',
475
+ entries: metadata.commands.map((command) => ({
476
+ id: command.name,
477
+ label: command.name.startsWith('/') ? command.name : `/${command.name}`,
478
+ status: 'configured',
479
+ description: command.description ?? command.argumentHint ?? undefined,
480
+ })),
481
+ },
482
+ {
483
+ id: 'agents',
484
+ label: 'Agents',
485
+ entries: metadata.agents.map((agent) => ({
486
+ id: agent.name,
487
+ label: agent.name,
488
+ status: 'configured',
489
+ description: agent.description ?? agent.model ?? undefined,
490
+ })),
491
+ },
372
492
  ],
373
493
  execution: {
374
494
  resolvedWorkspaceLabel: input.workspaceLabel,
375
495
  resolvedCwd: input.resolvedCwd,
496
+ workspaceRootId: input.workspaceRootId ?? null,
497
+ workspaceRelativePath: input.workspaceRelativePath ?? null,
376
498
  executionMode: input.executionMode,
377
499
  executionBranch: input.executionBranch,
378
500
  worktreePath: input.worktreePath,
@@ -495,6 +617,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
495
617
  systemPrompt: { type: 'preset', preset: 'claude_code' },
496
618
  tools: { type: 'preset', preset: 'claude_code' },
497
619
  permissionMode: config.permissionMode,
620
+ allowDangerouslySkipPermissions: true,
498
621
  cwd,
499
622
  includePartialMessages: true,
500
623
  maxTurns: 200,
@@ -534,7 +657,30 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
534
657
  if (toolName === 'ExitPlanMode') {
535
658
  try {
536
659
  const planId = randomUUID();
537
- const planApproval = buildPlanApprovalRequest(planId);
660
+ const allowedPrompts = Array.isArray(input.allowedPrompts)
661
+ ? input.allowedPrompts
662
+ .map((entry) => {
663
+ if (!entry || typeof entry !== 'object') {
664
+ return null;
665
+ }
666
+ const prompt = entry.prompt;
667
+ const tool = entry.tool;
668
+ if (typeof prompt !== 'string' || !prompt.trim()) {
669
+ return null;
670
+ }
671
+ return {
672
+ tool: typeof tool === 'string' && tool.trim() ? tool : 'Tool',
673
+ prompt,
674
+ };
675
+ })
676
+ .filter((entry) => Boolean(entry))
677
+ : [];
678
+ const planApproval = buildPlanApprovalRequest(planId, 'Plan ready for review.', allowedPrompts.length > 0 ? {
679
+ summary: 'Claude has prepared a plan and is asking permission to continue.',
680
+ allowedPrompts,
681
+ } : {
682
+ summary: 'Claude has prepared a plan and is asking permission to continue.',
683
+ });
538
684
  await client.sendMessage(conversationId, planApproval.text, {
539
685
  metadata: {
540
686
  ...planApproval.metadata,
@@ -594,6 +740,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
594
740
  currentTurnOpenedAt: null,
595
741
  lastAcceptedIntent: null,
596
742
  lastActivity: Date.now(),
743
+ typingKeepaliveTimer: null,
597
744
  closed: false,
598
745
  sdkSessionId: resumeSessionId || null,
599
746
  pendingReply: null,
@@ -650,6 +797,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
650
797
  session.pendingFinalDeliveryText = null;
651
798
  session.toolInProgress = false;
652
799
  session.turnState = 'idle';
800
+ stopVisibleWorkSignal();
653
801
  writeTurn();
654
802
  drainPendingInput();
655
803
  }
@@ -668,6 +816,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
668
816
  session.pendingFinalDeliveryText = finalText;
669
817
  session.state.state = 'running';
670
818
  session.turnState = 'streaming';
819
+ stopVisibleWorkSignal();
671
820
  streamingBuffer.replace(finalText);
672
821
  writeState();
673
822
  writeTurn();
@@ -719,6 +868,9 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
719
868
  executionMode: session.environment.mode,
720
869
  ...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
721
870
  ...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
871
+ ...(resolveExecutionFallbackReason(session.environment)
872
+ ? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
873
+ : {}),
722
874
  hostMode: true,
723
875
  isActive: true,
724
876
  }).catch(() => { });
@@ -737,6 +889,25 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
737
889
  : {}),
738
890
  }).catch(() => { });
739
891
  }
892
+ function refreshVisibleWorkSignal() {
893
+ if (session.closed)
894
+ return;
895
+ if (session.turnState !== 'thinking' && session.turnState !== 'tool')
896
+ return;
897
+ client.setTyping(conversationId, true, 'thinking').catch(() => { });
898
+ }
899
+ function startVisibleWorkSignal() {
900
+ refreshVisibleWorkSignal();
901
+ if (session.typingKeepaliveTimer)
902
+ return;
903
+ session.typingKeepaliveTimer = setInterval(refreshVisibleWorkSignal, 3500);
904
+ }
905
+ function stopVisibleWorkSignal() {
906
+ if (session.typingKeepaliveTimer) {
907
+ clearInterval(session.typingKeepaliveTimer);
908
+ session.typingKeepaliveTimer = null;
909
+ }
910
+ }
740
911
  async function markQueuedMessageAccepted(sourceMessageId, markAccepted) {
741
912
  if (!markAccepted || !sourceMessageId)
742
913
  return;
@@ -768,6 +939,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
768
939
  if (session.state.state === 'running' && intent === 'interrupt') {
769
940
  session.pendingInputs.unshift({ msg, intent, sourceMessageId, markAccepted });
770
941
  session.turnState = 'interrupted';
942
+ stopVisibleWorkSignal();
771
943
  writeTurn();
772
944
  void session.query.interrupt().catch(() => { });
773
945
  return;
@@ -808,18 +980,22 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
808
980
  if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
809
981
  session.toolInProgress = true;
810
982
  session.turnState = 'tool';
983
+ startVisibleWorkSignal();
811
984
  writeTurn();
812
985
  }
813
986
  else if (event?.type === 'content_block_stop' && session.toolInProgress) {
814
987
  session.toolInProgress = false;
815
988
  if (session.state.state === 'running') {
816
989
  session.turnState = 'thinking';
990
+ startVisibleWorkSignal();
817
991
  writeTurn();
818
992
  }
819
993
  }
820
994
  else if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
821
995
  session.toolInProgress = false;
822
996
  session.turnState = 'streaming';
997
+ stopVisibleWorkSignal();
998
+ client.setTyping(conversationId, false).catch(() => { });
823
999
  writeTurn();
824
1000
  onStreamDelta(event.delta.text);
825
1001
  }
@@ -840,9 +1016,10 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
840
1016
  session.pendingFinalText = null;
841
1017
  session.toolInProgress = false;
842
1018
  session.turnState = 'thinking';
843
- client.setTyping(conversationId, true, 'thinking').catch(() => { });
1019
+ startVisibleWorkSignal();
844
1020
  }
845
1021
  else if (state === 'idle') {
1022
+ stopVisibleWorkSignal();
846
1023
  client.setTyping(conversationId, false).catch(() => { });
847
1024
  }
848
1025
  else if (state === 'requires_action') {
@@ -853,6 +1030,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
853
1030
  }
854
1031
  session.toolInProgress = false;
855
1032
  session.turnState = 'waiting_input';
1033
+ stopVisibleWorkSignal();
856
1034
  client.setTyping(conversationId, false).catch(() => { });
857
1035
  }
858
1036
  writeState();
@@ -952,6 +1130,12 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
952
1130
  availableModels: modelList,
953
1131
  availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
954
1132
  availableExecutionModes: config.availableExecutionModes,
1133
+ runtimeDescriptor: buildClaudeRuntimeDescriptor({
1134
+ models: modelList,
1135
+ workspaces: buildPublicWorkspaceOptions(workspaceOptions),
1136
+ workspaceRoots: workspaceRootMetadata,
1137
+ executionModes: config.availableExecutionModes,
1138
+ }),
955
1139
  });
956
1140
  writeSessionState(conversationId, agentId, {
957
1141
  ...session.state,
@@ -960,6 +1144,9 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
960
1144
  executionMode: session.environment.mode,
961
1145
  ...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
962
1146
  ...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
1147
+ ...(resolveExecutionFallbackReason(session.environment)
1148
+ ? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
1149
+ : {}),
963
1150
  hostMode: true,
964
1151
  isActive: true,
965
1152
  }).catch(() => { });
@@ -981,12 +1168,24 @@ export async function main() {
981
1168
  options: {
982
1169
  cwd: { type: 'string' },
983
1170
  workspace: { type: 'string', multiple: true },
1171
+ 'workspace-root': { type: 'string', multiple: true },
984
1172
  },
985
1173
  strict: false,
986
1174
  });
987
1175
  workingDir = (typeof args.cwd === 'string' ? args.cwd : null) || process.cwd();
988
1176
  const configuredWorkspaces = (args.workspace ?? []).filter((value) => typeof value === 'string');
989
- workspaceOptions = buildConfiguredWorkspaceOptions(workingDir, configuredWorkspaces);
1177
+ const configuredWorkspaceRoots = (args['workspace-root'] ?? []).filter((value) => typeof value === 'string');
1178
+ const workspaceDiscovery = buildConfiguredWorkspaceOptionsWithRoots({
1179
+ primaryCwd: workingDir,
1180
+ configuredWorkspaces,
1181
+ workspaceRoots: configuredWorkspaceRoots,
1182
+ });
1183
+ workspaceOptions = workspaceDiscovery.workspaceOptions;
1184
+ workspaceRoots = workspaceDiscovery.workspaceRoots;
1185
+ workspaceRootMetadata = buildPublicWorkspaceRoots(workspaceRoots);
1186
+ for (const warning of workspaceDiscovery.warnings) {
1187
+ console.error(`[canon-host] ${warning}`);
1188
+ }
990
1189
  const { apiKey, agentId: profileAgentId, profile } = resolveCanonAgent({ logPrefix: '[canon-host]' });
991
1190
  console.error(`[canon-host] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
992
1191
  // ── Canon API client + RTDB auth ──
@@ -1023,6 +1222,7 @@ export async function main() {
1023
1222
  runtimeDescriptor: buildClaudeRuntimeDescriptor({
1024
1223
  models: runtimeModels,
1025
1224
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
1225
+ workspaceRoots: workspaceRootMetadata,
1026
1226
  executionModes: hostAvailableExecutionModes,
1027
1227
  }),
1028
1228
  };
@@ -1080,11 +1280,14 @@ export async function main() {
1080
1280
  descriptor: runtimeDescriptor.runtimeDescriptor ?? buildClaudeRuntimeDescriptor({
1081
1281
  models: runtimeModels,
1082
1282
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
1283
+ workspaceRoots: workspaceRootMetadata,
1083
1284
  executionModes: hostAvailableExecutionModes,
1084
1285
  }),
1085
1286
  metadata,
1086
1287
  workspaceLabel: workspace?.label ?? workspaceId ?? null,
1087
1288
  resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
1289
+ workspaceRootId: workspace?.workspaceRootId ?? null,
1290
+ workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
1088
1291
  executionMode: session?.environment.mode ?? null,
1089
1292
  executionBranch: session?.environment.branch ?? null,
1090
1293
  worktreePath: session?.environment.worktreePath ?? null,
@@ -1109,6 +1312,7 @@ export async function main() {
1109
1312
  runtimeDescriptor: buildClaudeRuntimeDescriptor({
1110
1313
  models: runtimeModels,
1111
1314
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
1315
+ workspaceRoots: workspaceRootMetadata,
1112
1316
  executionModes: hostAvailableExecutionModes,
1113
1317
  }),
1114
1318
  };
@@ -1123,6 +1327,7 @@ export async function main() {
1123
1327
  runtimeDescriptor: buildClaudeRuntimeDescriptor({
1124
1328
  models: runtimeModels,
1125
1329
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
1330
+ workspaceRoots: workspaceRootMetadata,
1126
1331
  executionModes: hostAvailableExecutionModes,
1127
1332
  }),
1128
1333
  };
@@ -1182,6 +1387,10 @@ export async function main() {
1182
1387
  if (!session)
1183
1388
  return;
1184
1389
  session.closed = true;
1390
+ if (session.typingKeepaliveTimer) {
1391
+ clearInterval(session.typingKeepaliveTimer);
1392
+ session.typingKeepaliveTimer = null;
1393
+ }
1185
1394
  releaseConversationEnvironment(session.environment);
1186
1395
  // Save SDK session ID for future resume
1187
1396
  if (session.sdkSessionId) {
@@ -1370,7 +1579,12 @@ export async function main() {
1370
1579
  const message = error instanceof Error ? error.message : String(error);
1371
1580
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1372
1581
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
1373
- await client.sendMessage(convoId, `I couldn't start a coding session for this workspace: ${userMessage}`).catch(() => { });
1582
+ await client.sendMessage(convoId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1583
+ metadata: {
1584
+ turnSemantics: 'turn_complete',
1585
+ turnComplete: true,
1586
+ },
1587
+ }).catch(() => { });
1374
1588
  return;
1375
1589
  }
1376
1590
  const turnMetadata = normalizeTurnMetadata(m.metadata);
@@ -1467,6 +1681,9 @@ export async function main() {
1467
1681
  executionMode: session.environment.mode,
1468
1682
  ...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
1469
1683
  ...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
1684
+ ...(resolveExecutionFallbackReason(session.environment)
1685
+ ? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
1686
+ : {}),
1470
1687
  hostMode: true,
1471
1688
  isActive: true,
1472
1689
  }).catch(() => { });
package/dist/server.js CHANGED
@@ -26,6 +26,10 @@ let agentContext = null;
26
26
  let approvalManager = null;
27
27
  let approvalServer = null;
28
28
  const conversationCache = new Map();
29
+ const TURN_COMPLETE_METADATA = {
30
+ turnSemantics: 'turn_complete',
31
+ turnComplete: true,
32
+ };
29
33
  /** The conversation that most recently triggered work (for approval routing) */
30
34
  let triggerConversationId = null;
31
35
  function getPrimaryAttachment(message) {
@@ -221,7 +225,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
221
225
  case 'reply': {
222
226
  const conversationId = args.conversation_id;
223
227
  const text = args.text;
224
- const result = await client.sendMessage(conversationId, text);
228
+ const result = await client.sendMessage(conversationId, text, {
229
+ metadata: TURN_COMPLETE_METADATA,
230
+ });
225
231
  return {
226
232
  content: [
227
233
  {
@@ -261,6 +267,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
261
267
  else {
262
268
  opts.contentType = contentType;
263
269
  }
270
+ opts.metadata = {
271
+ ...(opts.metadata ?? {}),
272
+ ...TURN_COMPLETE_METADATA,
273
+ };
264
274
  const result = await client.sendMessage(conversationId, text, opts);
265
275
  return {
266
276
  content: [
@@ -288,6 +298,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
288
298
  const messageOptions = typeof args.message_options === 'object' && args.message_options != null
289
299
  ? args.message_options
290
300
  : undefined;
301
+ const linkedMessageOptions = messageOptions;
291
302
  const result = await client.sendLinkedMessage({
292
303
  sourceConversationId,
293
304
  targetConversationId,
@@ -308,11 +319,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
308
319
  targetContext: targetContext,
309
320
  }
310
321
  : {}),
311
- ...(messageOptions
322
+ ...(linkedMessageOptions
312
323
  ? {
313
- messageOptions: messageOptions,
324
+ messageOptions: {
325
+ ...linkedMessageOptions,
326
+ metadata: {
327
+ ...(linkedMessageOptions.metadata ?? {}),
328
+ ...TURN_COMPLETE_METADATA,
329
+ },
330
+ },
314
331
  }
315
- : {}),
332
+ : { messageOptions: { metadata: TURN_COMPLETE_METADATA } }),
316
333
  });
317
334
  return {
318
335
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "^0.2.101",
34
- "@canonmsg/agent-sdk": "^0.9.0",
35
- "@canonmsg/core": "^0.8.0",
34
+ "@canonmsg/agent-sdk": "^0.9.2",
35
+ "@canonmsg/core": "^0.10.0",
36
36
  "@modelcontextprotocol/sdk": "^1.12.0"
37
37
  },
38
38
  "engines": {