@bike4mind/cli 0.2.82 → 0.4.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/dist/index.mjs CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { n as useCliStore, t as selectActiveBackgroundAgents } from "./store-Dw1nZX2Y.mjs";
3
- import { A as DEFAULT_MAX_ITERATIONS, B as ReActAgent, C as loadContextFiles, D as generateCliTools, E as PermissionManager, F as setWebSocketToolExecutor, G as OAuthClient, H as CheckpointStore, I as OllamaBackend, J as searchCommands, K as hasFileReferences, L as buildCoreSystemPrompt, M as DEFAULT_THOROUGHNESS, N as clearFeatureModuleTools, O as ALWAYS_DENIED_FOR_AGENTS, P as registerFeatureModuleTools, Q as warmFileCache, R as buildSkillsPromptSection, S as extractCompactInstructions, T as getEnvironmentName, U as CommandHistoryStore, V as CustomCommandStore, W as SessionStore, X as formatFileSize, Y as mergeCommands, Z as searchFiles, _ as WebSocketLlmBackend, a as createCoordinateTaskTool, b as substituteArguments, c as createAgentDelegateTool, d as createSkillTool, f as parseAgentConfig, g as FallbackLlmBackend, h as WebSocketConnectionManager, i as createWriteTodosTool, j as DEFAULT_RETRY_CONFIG, k as DEFAULT_AGENT_MODEL, l as AgentStore, m as WebSocketToolExecutor, n as createFindDefinitionTool, o as createBackgroundAgentTools, p as ApiClient, q as processFileReferences, r as createTodoStore, s as BackgroundAgentManager, t as createGetFileStructureTool, u as SubagentOrchestrator, v as ServerLlmBackend, w as getApiUrl, x as formatStep, y as McpManager, z as isReadOnlyTool } from "./tools-C0eJHV0Y.mjs";
4
- import { Dt as validateJupyterKernelName, Ot as validateNotebookPath$1, g as ChatModels, m as CREDIT_DEDUCT_TRANSACTION_TYPES, n as logger, t as ConfigStore } from "./ConfigStore-DTyUBb3A.mjs";
5
- import { i as version, t as checkForUpdate } from "./updateChecker-CPr5OfMn.mjs";
2
+ import { n as useCliStore, t as selectActiveBackgroundAgents } from "./store-B7-LLvvx.mjs";
3
+ import { A as DEFAULT_MAX_ITERATIONS, B as ReActAgent, C as loadContextFiles, D as generateCliTools, E as PermissionManager, F as setWebSocketToolExecutor, G as OAuthClient, H as CheckpointStore, I as OllamaBackend, J as searchCommands, K as hasFileReferences, L as buildCoreSystemPrompt, M as DEFAULT_THOROUGHNESS, N as clearFeatureModuleTools, O as ALWAYS_DENIED_FOR_AGENTS, P as registerFeatureModuleTools, Q as warmFileCache, R as buildSkillsPromptSection, S as extractCompactInstructions, T as getEnvironmentName, U as CommandHistoryStore, V as CustomCommandStore, W as SessionStore, X as formatFileSize, Y as mergeCommands, Z as searchFiles, _ as WebSocketLlmBackend, a as createCoordinateTaskTool, b as substituteArguments, c as createAgentDelegateTool, d as createSkillTool, f as parseAgentConfig, g as FallbackLlmBackend, h as WebSocketConnectionManager, i as createWriteTodosTool, j as DEFAULT_RETRY_CONFIG, k as DEFAULT_AGENT_MODEL, l as AgentStore, m as WebSocketToolExecutor, n as createFindDefinitionTool, o as createBackgroundAgentTools, p as ApiClient, q as processFileReferences, r as createTodoStore, s as BackgroundAgentManager, t as createGetFileStructureTool, u as SubagentOrchestrator, v as ServerLlmBackend, w as getApiUrl, x as formatStep, y as McpManager, z as isReadOnlyTool } from "./tools-DiSJh1Dv.mjs";
4
+ import { Mt as validateNotebookPath$1, g as ChatModels, jt as validateJupyterKernelName, m as CREDIT_DEDUCT_TRANSACTION_TYPES, n as logger, t as ConfigStore } from "./ConfigStore-DBUmvCfe.mjs";
5
+ import { i as version, t as checkForUpdate } from "./updateChecker-DcJC1C8S.mjs";
6
6
  import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
7
7
  import { Box, Static, Text, render, useApp, useInput } from "ink";
8
8
  import { execSync } from "child_process";
9
9
  import { randomBytes, randomUUID } from "crypto";
10
10
  import { existsSync, promises, readFileSync, statSync } from "fs";
11
11
  import { homedir } from "os";
12
- import path, { extname } from "path";
12
+ import path, { basename, extname, join } from "path";
13
13
  import { v4 } from "uuid";
14
14
  import * as path$1 from "node:path";
15
15
  import Spinner from "ink-spinner";
@@ -1980,14 +1980,14 @@ function App({ onMessage, onBackgroundCompletion, onCommand, onBashCommand, onPe
1980
1980
  args: permissionPrompt.args,
1981
1981
  preview: permissionPrompt.preview,
1982
1982
  canBeTrusted: permissionPrompt.canBeTrusted,
1983
- onResponse: onPermissionResponse
1983
+ onResponse: (response) => onPermissionResponse(response, permissionPrompt.id)
1984
1984
  })), userQuestionPrompt && /* @__PURE__ */ React.createElement(Box, {
1985
1985
  key: userQuestionPrompt.id,
1986
1986
  flexDirection: "column",
1987
1987
  paddingX: 1
1988
1988
  }, /* @__PURE__ */ React.createElement(UserQuestionPrompt, {
1989
1989
  payload: userQuestionPrompt.payload,
1990
- onResponse: onUserQuestionResponse
1990
+ onResponse: (response) => onUserQuestionResponse(response, userQuestionPrompt.id)
1991
1991
  })), !permissionPrompt && !userQuestionPrompt && /* @__PURE__ */ React.createElement(AgentThinking, null), /* @__PURE__ */ React.createElement(BackgroundAgentStatus, null), /* @__PURE__ */ React.createElement(CompletedGroupNotification, null), exitRequested && /* @__PURE__ */ React.createElement(Box, {
1992
1992
  paddingX: 1,
1993
1993
  marginBottom: 1
@@ -3439,6 +3439,27 @@ var TavernService = class {
3439
3439
  async abortHeartbeats() {
3440
3440
  await this.apiClient.post("/api/tavern/abort-heartbeats", { abort: true });
3441
3441
  }
3442
+ async getQuestPlan(planId) {
3443
+ return this.apiClient.get(`/api/quest-master-plans/${encodeURIComponent(planId)}`);
3444
+ }
3445
+ async updateReviewGate(planId, questId, subQuestId, reviewStatus, reviewNote) {
3446
+ return this.apiClient.post(`/api/quest-master-plans/${encodeURIComponent(planId)}/review-gate`, {
3447
+ questId,
3448
+ subQuestId,
3449
+ reviewStatus,
3450
+ reviewNote
3451
+ });
3452
+ }
3453
+ async updateSubQuestProgress(planId, questId, subQuestId, updates) {
3454
+ return this.apiClient.post(`/api/quest-master-plans/${encodeURIComponent(planId)}/subquest-progress`, {
3455
+ questId,
3456
+ subQuestId,
3457
+ ...updates
3458
+ });
3459
+ }
3460
+ async updateHandoff(planId, handoff) {
3461
+ return this.apiClient.post(`/api/quest-master-plans/${encodeURIComponent(planId)}/handoff`, handoff);
3462
+ }
3442
3463
  };
3443
3464
  //#endregion
3444
3465
  //#region src/features/tavern/types.ts
@@ -3626,6 +3647,109 @@ z.object({
3626
3647
  error: z.string().optional()
3627
3648
  }))
3628
3649
  });
3650
+ /** Sub-quest within a quest */
3651
+ const SubQuestSchema = z.object({
3652
+ id: z.string(),
3653
+ title: z.string(),
3654
+ status: z.enum([
3655
+ "not_started",
3656
+ "in_progress",
3657
+ "completed",
3658
+ "skipped",
3659
+ "deleted"
3660
+ ]),
3661
+ questId: z.string().optional(),
3662
+ startedAt: z.number().optional(),
3663
+ evidence: z.string().optional(),
3664
+ reviewGate: z.boolean().optional(),
3665
+ reviewStatus: z.enum([
3666
+ "pending",
3667
+ "approved",
3668
+ "rejected"
3669
+ ]).optional(),
3670
+ reviewNote: z.string().optional()
3671
+ });
3672
+ /** Quest containing sub-quests */
3673
+ const QuestDataSchema = z.object({
3674
+ id: z.string(),
3675
+ title: z.string(),
3676
+ description: z.string(),
3677
+ complexity: z.string(),
3678
+ subQuests: z.array(SubQuestSchema)
3679
+ });
3680
+ /** Handoff state for session continuity */
3681
+ const HandoffSchema = z.object({
3682
+ summary: z.string(),
3683
+ nextSteps: z.array(z.string()),
3684
+ pendingDecisions: z.array(z.string()),
3685
+ blockers: z.array(z.string()),
3686
+ lastUpdatedBy: z.string(),
3687
+ updatedAt: z.string()
3688
+ });
3689
+ z.object({
3690
+ _id: z.string(),
3691
+ notebookId: z.string(),
3692
+ goal: z.string(),
3693
+ quests: z.array(QuestDataSchema),
3694
+ state: z.enum([
3695
+ "draft",
3696
+ "active",
3697
+ "paused",
3698
+ "completed",
3699
+ "archived"
3700
+ ]).optional(),
3701
+ handoff: HandoffSchema.optional(),
3702
+ metrics: z.object({
3703
+ totalTimeSpent: z.number(),
3704
+ completionRate: z.number(),
3705
+ subQuestsCompleted: z.number(),
3706
+ subQuestsTotal: z.number(),
3707
+ lastProgress: z.string().optional()
3708
+ }).optional()
3709
+ });
3710
+ z.object({
3711
+ planId: z.string(),
3712
+ questId: z.string(),
3713
+ subQuestId: z.string(),
3714
+ reviewStatus: z.enum([
3715
+ "pending",
3716
+ "approved",
3717
+ "rejected"
3718
+ ]),
3719
+ reviewNote: z.string().optional()
3720
+ });
3721
+ z.object({
3722
+ planId: z.string(),
3723
+ questId: z.string(),
3724
+ subQuestId: z.string(),
3725
+ status: z.enum([
3726
+ "not_started",
3727
+ "in_progress",
3728
+ "completed",
3729
+ "skipped",
3730
+ "deleted"
3731
+ ]).optional(),
3732
+ evidence: z.string().optional(),
3733
+ timeSpent: z.number().optional()
3734
+ });
3735
+ z.object({
3736
+ planId: z.string(),
3737
+ summary: z.string(),
3738
+ nextSteps: z.array(z.string()),
3739
+ pendingDecisions: z.array(z.string()),
3740
+ blockers: z.array(z.string())
3741
+ });
3742
+ z.object({
3743
+ success: z.boolean(),
3744
+ plan: z.unknown().optional(),
3745
+ metrics: z.object({
3746
+ totalTimeSpent: z.number(),
3747
+ completionRate: z.number(),
3748
+ subQuestsCompleted: z.number(),
3749
+ subQuestsTotal: z.number(),
3750
+ lastProgress: z.string().optional()
3751
+ }).optional()
3752
+ });
3629
3753
  //#endregion
3630
3754
  //#region src/features/tavern/TavernActivityStream.ts
3631
3755
  /**
@@ -3660,6 +3784,39 @@ var TavernActivityStream = class {
3660
3784
  };
3661
3785
  //#endregion
3662
3786
  //#region src/features/tavern/tavernTools.ts
3787
+ const GetQuestPlanParamsSchema = z.object({ plan_id: z.string().min(1) });
3788
+ const UpdateReviewGateParamsSchema = z.object({
3789
+ plan_id: z.string().min(1),
3790
+ quest_id: z.string().min(1),
3791
+ sub_quest_id: z.string().min(1),
3792
+ review_status: z.enum([
3793
+ "pending",
3794
+ "approved",
3795
+ "rejected"
3796
+ ]),
3797
+ review_note: z.string().optional()
3798
+ });
3799
+ const UpdateQuestProgressParamsSchema = z.object({
3800
+ plan_id: z.string().min(1),
3801
+ quest_id: z.string().min(1),
3802
+ sub_quest_id: z.string().min(1),
3803
+ status: z.enum([
3804
+ "not_started",
3805
+ "in_progress",
3806
+ "completed",
3807
+ "skipped",
3808
+ "deleted"
3809
+ ]).optional(),
3810
+ evidence: z.string().optional(),
3811
+ time_spent: z.number().min(0).optional()
3812
+ });
3813
+ const WriteHandoffParamsSchema = z.object({
3814
+ plan_id: z.string().min(1),
3815
+ summary: z.string().min(1),
3816
+ next_steps: z.array(z.string()),
3817
+ pending_decisions: z.array(z.string()),
3818
+ blockers: z.array(z.string())
3819
+ });
3663
3820
  /**
3664
3821
  * Factory that creates ICompletionOptionTools[] for the Tavern feature.
3665
3822
  *
@@ -3682,7 +3839,11 @@ function createTavernTools(service) {
3682
3839
  createToggleHeartbeatsTool(service),
3683
3840
  createTriggerHeartbeatTool(service),
3684
3841
  createAbortHeartbeatsTool(service),
3685
- createStatusTool(service)
3842
+ createStatusTool(service),
3843
+ createGetQuestPlanTool(service),
3844
+ createUpdateReviewGateTool(service),
3845
+ createUpdateQuestProgressTool(service),
3846
+ createWriteHandoffTool(service)
3686
3847
  ];
3687
3848
  }
3688
3849
  function createListAgentsTool(service) {
@@ -4211,6 +4372,187 @@ function createStatusTool(service) {
4211
4372
  }
4212
4373
  };
4213
4374
  }
4375
+ function createGetQuestPlanTool(service) {
4376
+ return {
4377
+ toolSchema: {
4378
+ name: "tavern_get_quest_plan",
4379
+ description: "Fetch a quest master plan by ID. Returns the full plan with all quests, sub-quests, review gate status, handoff state, and progress metrics. Use this to check which sub-quests have review gates and their current status.",
4380
+ parameters: {
4381
+ type: "object",
4382
+ properties: { plan_id: {
4383
+ type: "string",
4384
+ description: "The MongoDB ObjectId of the quest master plan"
4385
+ } },
4386
+ required: ["plan_id"]
4387
+ }
4388
+ },
4389
+ toolFn: async (params) => {
4390
+ const { plan_id } = GetQuestPlanParamsSchema.parse(params);
4391
+ const result = await service.getQuestPlan(plan_id);
4392
+ return JSON.stringify(result);
4393
+ }
4394
+ };
4395
+ }
4396
+ function createUpdateReviewGateTool(service) {
4397
+ return {
4398
+ toolSchema: {
4399
+ name: "tavern_update_review_gate",
4400
+ description: "Approve or reject a review gate on a sub-quest. Review gates are human approval checkpoints — when a sub-quest has reviewGate: true, the AI must stop and wait for human approval before proceeding.",
4401
+ parameters: {
4402
+ type: "object",
4403
+ properties: {
4404
+ plan_id: {
4405
+ type: "string",
4406
+ description: "The MongoDB ObjectId of the quest master plan"
4407
+ },
4408
+ quest_id: {
4409
+ type: "string",
4410
+ description: "The ID of the parent quest"
4411
+ },
4412
+ sub_quest_id: {
4413
+ type: "string",
4414
+ description: "The ID of the sub-quest with the review gate"
4415
+ },
4416
+ review_status: {
4417
+ type: "string",
4418
+ description: "The review decision",
4419
+ enum: [
4420
+ "pending",
4421
+ "approved",
4422
+ "rejected"
4423
+ ]
4424
+ },
4425
+ review_note: {
4426
+ type: "string",
4427
+ description: "Optional note explaining the review decision"
4428
+ }
4429
+ },
4430
+ required: [
4431
+ "plan_id",
4432
+ "quest_id",
4433
+ "sub_quest_id",
4434
+ "review_status"
4435
+ ]
4436
+ }
4437
+ },
4438
+ toolFn: async (params) => {
4439
+ const { plan_id, quest_id, sub_quest_id, review_status, review_note } = UpdateReviewGateParamsSchema.parse(params);
4440
+ const result = await service.updateReviewGate(plan_id, quest_id, sub_quest_id, review_status, review_note);
4441
+ return JSON.stringify(result);
4442
+ }
4443
+ };
4444
+ }
4445
+ function createUpdateQuestProgressTool(service) {
4446
+ return {
4447
+ toolSchema: {
4448
+ name: "tavern_update_quest_progress",
4449
+ description: "Update a sub-quest's progress. Set status to track completion, add evidence of what was accomplished, and optionally record time spent. Setting status to \"in_progress\" auto-resumes a paused plan.",
4450
+ parameters: {
4451
+ type: "object",
4452
+ properties: {
4453
+ plan_id: {
4454
+ type: "string",
4455
+ description: "The MongoDB ObjectId of the quest master plan"
4456
+ },
4457
+ quest_id: {
4458
+ type: "string",
4459
+ description: "The ID of the parent quest"
4460
+ },
4461
+ sub_quest_id: {
4462
+ type: "string",
4463
+ description: "The ID of the sub-quest to update"
4464
+ },
4465
+ status: {
4466
+ type: "string",
4467
+ description: "New status for the sub-quest",
4468
+ enum: [
4469
+ "not_started",
4470
+ "in_progress",
4471
+ "completed",
4472
+ "skipped",
4473
+ "deleted"
4474
+ ]
4475
+ },
4476
+ evidence: {
4477
+ type: "string",
4478
+ description: "Evidence of completion — links to artifacts, descriptions of output, or references to results"
4479
+ },
4480
+ time_spent: {
4481
+ type: "number",
4482
+ description: "Time spent on this sub-quest in milliseconds"
4483
+ }
4484
+ },
4485
+ required: [
4486
+ "plan_id",
4487
+ "quest_id",
4488
+ "sub_quest_id"
4489
+ ]
4490
+ }
4491
+ },
4492
+ toolFn: async (params) => {
4493
+ const { plan_id, quest_id, sub_quest_id, status, evidence, time_spent } = UpdateQuestProgressParamsSchema.parse(params);
4494
+ const updates = {};
4495
+ if (status !== void 0) updates.status = status;
4496
+ if (evidence !== void 0) updates.evidence = evidence;
4497
+ if (time_spent !== void 0) updates.timeSpent = time_spent;
4498
+ const result = await service.updateSubQuestProgress(plan_id, quest_id, sub_quest_id, updates);
4499
+ return JSON.stringify(result);
4500
+ }
4501
+ };
4502
+ }
4503
+ function createWriteHandoffTool(service) {
4504
+ return {
4505
+ toolSchema: {
4506
+ name: "tavern_write_handoff",
4507
+ description: "Write a handoff state for session continuity. Called when ending a session so the next session can resume with full context. Includes a summary of progress, next steps, pending decisions, and blockers.",
4508
+ parameters: {
4509
+ type: "object",
4510
+ properties: {
4511
+ plan_id: {
4512
+ type: "string",
4513
+ description: "The MongoDB ObjectId of the quest master plan"
4514
+ },
4515
+ summary: {
4516
+ type: "string",
4517
+ description: "Summary of what was accomplished in this session"
4518
+ },
4519
+ next_steps: {
4520
+ type: "array",
4521
+ items: { type: "string" },
4522
+ description: "List of next steps for the following session"
4523
+ },
4524
+ pending_decisions: {
4525
+ type: "array",
4526
+ items: { type: "string" },
4527
+ description: "Decisions that still need to be made"
4528
+ },
4529
+ blockers: {
4530
+ type: "array",
4531
+ items: { type: "string" },
4532
+ description: "Current blockers preventing progress"
4533
+ }
4534
+ },
4535
+ required: [
4536
+ "plan_id",
4537
+ "summary",
4538
+ "next_steps",
4539
+ "pending_decisions",
4540
+ "blockers"
4541
+ ]
4542
+ }
4543
+ },
4544
+ toolFn: async (params) => {
4545
+ const { plan_id, summary, next_steps, pending_decisions, blockers } = WriteHandoffParamsSchema.parse(params);
4546
+ const result = await service.updateHandoff(plan_id, {
4547
+ summary,
4548
+ nextSteps: next_steps,
4549
+ pendingDecisions: pending_decisions,
4550
+ blockers
4551
+ });
4552
+ return JSON.stringify(result);
4553
+ }
4554
+ };
4555
+ }
4214
4556
  //#endregion
4215
4557
  //#region src/features/tavern/TavernModule.ts
4216
4558
  /** Icons for heartbeat log actions shown in /tavern command */
@@ -4274,9 +4616,23 @@ Available actions:
4274
4616
  - **tavern_trigger_heartbeat**: Manually trigger a heartbeat cycle
4275
4617
  - **tavern_abort_heartbeats**: Emergency stop all in-flight heartbeats
4276
4618
  - **tavern_status**: Quick overview of agents, quests, and gates — good for situational awareness
4619
+ - **tavern_get_quest_plan**: Fetch a quest master plan with review gate status and handoff state
4620
+ - **tavern_update_review_gate**: Approve or reject a review gate on a sub-quest
4621
+ - **tavern_update_quest_progress**: Update sub-quest status and record evidence of completion
4622
+ - **tavern_write_handoff**: Write session handoff for continuity across sessions
4277
4623
 
4278
4624
  When the user mentions talking to agents, checking the quest board, or managing the tavern, use these tools.
4279
- Agents have personalities, moods, quests, and memories — they are autonomous entities, not chatbots.`;
4625
+ Agents have personalities, moods, quests, and memories — they are autonomous entities, not chatbots.
4626
+
4627
+ ## Quest Workflow (Review Gates)
4628
+ When working on a quest plan with review gates (reviewGate: true on sub-quests), you MUST:
4629
+ 1. Check review gate status with tavern_get_quest_plan before proceeding past a gated step
4630
+ 2. If the next sub-quest has reviewGate: true and reviewStatus is not 'approved', STOP and inform the user
4631
+ 3. Record evidence of completion with tavern_update_quest_progress when finishing a sub-quest
4632
+ 4. Write a handoff with tavern_write_handoff before ending a session with an active quest plan
4633
+
4634
+ ## Session Handoff & Resume
4635
+ When the user runs /quest resume <plan_id>, read the handoff context and continue from where the previous session left off. The handoff contains: summary of prior work, next steps, pending decisions, and blockers.`;
4280
4636
  }
4281
4637
  getCommands() {
4282
4638
  return [{
@@ -4301,6 +4657,95 @@ Agents have personalities, moods, quests, and memories — they are autonomous e
4301
4657
  }
4302
4658
  console.log("");
4303
4659
  }
4660
+ }, {
4661
+ name: "quest",
4662
+ description: "Quest workflow commands: review gates, resume from handoff",
4663
+ execute: async (args) => {
4664
+ const subCommand = args[0];
4665
+ if (subCommand === "resume") {
4666
+ const planId = args[1];
4667
+ if (!planId) {
4668
+ console.log("\nUsage: /quest resume <plan_id>");
4669
+ console.log(" Loads the quest plan and displays the session handoff for continuity.\n");
4670
+ return;
4671
+ }
4672
+ try {
4673
+ const plan = await this.service.getQuestPlan(planId);
4674
+ console.log(`\nQuest Plan: ${plan.goal}`);
4675
+ console.log(`State: ${plan.state ?? "unknown"}`);
4676
+ if (plan.metrics) {
4677
+ const pct = Math.round(plan.metrics.completionRate * 100);
4678
+ console.log(`Progress: ${plan.metrics.subQuestsCompleted}/${plan.metrics.subQuestsTotal} sub-quests (${pct}%)`);
4679
+ }
4680
+ if (!plan.handoff) {
4681
+ console.log("\n No handoff found — this plan has no saved session context.");
4682
+ console.log(" The AI can still read the plan via tavern_get_quest_plan.\n");
4683
+ return;
4684
+ }
4685
+ const { handoff } = plan;
4686
+ console.log(`\nSession Handoff (${new Date(handoff.updatedAt).toLocaleString()}):`);
4687
+ console.log(`\n Summary: ${handoff.summary}`);
4688
+ if (handoff.nextSteps.length > 0) {
4689
+ console.log("\n Next Steps:");
4690
+ for (const step of handoff.nextSteps) console.log(` \u2022 ${step}`);
4691
+ }
4692
+ if (handoff.pendingDecisions.length > 0) {
4693
+ console.log("\n Pending Decisions:");
4694
+ for (const decision of handoff.pendingDecisions) console.log(` \u2753 ${decision}`);
4695
+ }
4696
+ if (handoff.blockers.length > 0) {
4697
+ console.log("\n Blockers:");
4698
+ for (const blocker of handoff.blockers) console.log(` \u{1F6D1} ${blocker}`);
4699
+ }
4700
+ console.log("");
4701
+ } catch (error) {
4702
+ const message = error instanceof Error ? error.message : String(error);
4703
+ console.log(`\nError fetching quest plan: ${message}\n`);
4704
+ }
4705
+ return;
4706
+ }
4707
+ if (subCommand === "review") {
4708
+ const planId = args[1];
4709
+ if (!planId) {
4710
+ console.log("\nUsage: /quest review <plan_id>");
4711
+ console.log(" Fetches the quest plan and shows sub-quests with pending review gates.\n");
4712
+ return;
4713
+ }
4714
+ try {
4715
+ const plan = await this.service.getQuestPlan(planId);
4716
+ const pendingGates = [];
4717
+ for (const quest of plan.quests) for (const sq of quest.subQuests) if (sq.reviewGate) pendingGates.push({
4718
+ questTitle: quest.title,
4719
+ questId: quest.id,
4720
+ subQuestTitle: sq.title,
4721
+ subQuestId: sq.id,
4722
+ status: sq.reviewStatus ?? "pending"
4723
+ });
4724
+ if (pendingGates.length === 0) {
4725
+ console.log(`\nQuest Plan: ${plan.goal}`);
4726
+ console.log(" No review gates configured in this plan.\n");
4727
+ return;
4728
+ }
4729
+ console.log(`\nQuest Plan: ${plan.goal}`);
4730
+ console.log(`State: ${plan.state ?? "unknown"}\n`);
4731
+ console.log("Review Gates:");
4732
+ for (const gate of pendingGates) {
4733
+ const icon = gate.status === "approved" ? "✅" : gate.status === "rejected" ? "❌" : "⏸️";
4734
+ console.log(` ${icon} [${gate.status}] ${gate.questTitle} > ${gate.subQuestTitle}`);
4735
+ console.log(` quest_id: ${gate.questId} sub_quest_id: ${gate.subQuestId}`);
4736
+ }
4737
+ console.log("\n To approve: ask the AI to approve a review gate, or use tavern_update_review_gate tool.\n");
4738
+ } catch (error) {
4739
+ const message = error instanceof Error ? error.message : String(error);
4740
+ console.log(`\nError fetching quest plan: ${message}\n`);
4741
+ }
4742
+ return;
4743
+ }
4744
+ console.log("\nUsage:");
4745
+ console.log(" /quest review <plan_id> — Show review gates and their status");
4746
+ console.log(" /quest resume <plan_id> — Load handoff and resume from where you left off");
4747
+ console.log("");
4748
+ }
4304
4749
  }];
4305
4750
  }
4306
4751
  registerWsHandlers(wsManager) {
@@ -4311,6 +4756,224 @@ Agents have personalities, moods, quests, and memories — they are autonomous e
4311
4756
  }
4312
4757
  };
4313
4758
  //#endregion
4759
+ //#region src/features/bridgePresence/BridgePresence.ts
4760
+ const DEFAULT_PORT = Number(process.env.CC_BRIDGE_PORT ?? 48732);
4761
+ const CONFIG_PATH = join(homedir(), ".b4m", "cc-bridge.json");
4762
+ const ANNOUNCE_TIMEOUT_MS = 2e3;
4763
+ async function readBridgeConfig() {
4764
+ try {
4765
+ const raw = await promises.readFile(CONFIG_PATH, "utf8");
4766
+ const parsed = JSON.parse(raw);
4767
+ if (typeof parsed.hookSecret !== "string" || !parsed.hookSecret) return null;
4768
+ return {
4769
+ port: typeof parsed.port === "number" ? parsed.port : DEFAULT_PORT,
4770
+ hookSecret: parsed.hookSecret
4771
+ };
4772
+ } catch {
4773
+ return null;
4774
+ }
4775
+ }
4776
+ var BridgePresence = class {
4777
+ constructor() {
4778
+ this.config = null;
4779
+ this.instanceId = null;
4780
+ this.ws = null;
4781
+ this.callbacks = {};
4782
+ this.started = false;
4783
+ this.stopped = false;
4784
+ this.reconnectTimer = null;
4785
+ this.reconnectAttempts = 0;
4786
+ this.announceRetryTimer = null;
4787
+ this.announceAttempts = 0;
4788
+ this.startOpts = null;
4789
+ this.pendingWorkspaceName = null;
4790
+ this.pendingCapabilities = null;
4791
+ this.pendingSource = null;
4792
+ this.emitQueue = Promise.resolve();
4793
+ }
4794
+ setCallbacks(cbs) {
4795
+ this.callbacks = cbs;
4796
+ }
4797
+ /**
4798
+ * Probe the local bridge and, if present, announce this CLI session.
4799
+ * Returns true iff the announce succeeded (tavern presence is active).
4800
+ * Safe to call multiple times — second call no-ops.
4801
+ *
4802
+ * If announce fails (bridge absent or not yet up), a background retry
4803
+ * loop keeps trying with bounded backoff so the sprite appears when the
4804
+ * bridge comes online later in the CLI's lifetime.
4805
+ */
4806
+ async start(opts) {
4807
+ if (this.started) return this.instanceId !== null;
4808
+ this.started = true;
4809
+ const config = await readBridgeConfig();
4810
+ if (!config) {
4811
+ logger.debug("[tavern] cc-bridge not configured; CLI runs without tavern presence");
4812
+ return false;
4813
+ }
4814
+ this.config = config;
4815
+ this.startOpts = opts;
4816
+ this.pendingWorkspaceName = opts.workspaceName ?? (basename(opts.workspacePath) || "workspace");
4817
+ this.pendingCapabilities = opts.capabilities ?? ["interactive"];
4818
+ this.pendingSource = opts.source ?? "b4m-cli";
4819
+ return this.attemptAnnounce();
4820
+ }
4821
+ /** One announce attempt. Schedules a retry on failure; wires up the
4822
+ * command WS + initial status on success. Idempotent: re-entering after
4823
+ * a successful announce short-circuits at the instanceId guard. */
4824
+ async attemptAnnounce() {
4825
+ if (this.stopped || !this.config || !this.startOpts) return false;
4826
+ if (this.instanceId) return true;
4827
+ const instanceId = v4();
4828
+ const workspaceName = this.pendingWorkspaceName;
4829
+ const capabilities = this.pendingCapabilities;
4830
+ const source = this.pendingSource;
4831
+ if (!await this.announce({
4832
+ instanceId,
4833
+ source,
4834
+ workspaceName,
4835
+ workspacePath: this.startOpts.workspacePath,
4836
+ capabilities
4837
+ })) {
4838
+ this.scheduleAnnounceRetry();
4839
+ return false;
4840
+ }
4841
+ this.instanceId = instanceId;
4842
+ this.announceAttempts = 0;
4843
+ logger.info(`[tavern] announced ${workspaceName} to cc-bridge on 127.0.0.1:${this.config.port ?? DEFAULT_PORT}`);
4844
+ this.connectCommandWs();
4845
+ this.emitEvent({
4846
+ type: "status",
4847
+ status: "idle"
4848
+ });
4849
+ return true;
4850
+ }
4851
+ scheduleAnnounceRetry() {
4852
+ if (this.stopped || this.announceRetryTimer) return;
4853
+ this.announceAttempts += 1;
4854
+ const delay = Math.min(1e3 * 2 ** (this.announceAttempts - 1), 3e4);
4855
+ this.announceRetryTimer = setTimeout(() => {
4856
+ this.announceRetryTimer = null;
4857
+ this.attemptAnnounce();
4858
+ }, delay);
4859
+ }
4860
+ /** Emit an event for this session. No-op if the bridge isn't up. Events
4861
+ * leave in strict order — see `emitQueue` comment. */
4862
+ async emitEvent(event) {
4863
+ if (!this.config || !this.instanceId) return;
4864
+ const task = () => this.post("/event", {
4865
+ instanceId: this.instanceId,
4866
+ event
4867
+ }).catch((err) => logger.info(`[tavern] emitEvent ${event.type} failed: ${err.message}`));
4868
+ this.emitQueue = this.emitQueue.then(task, task);
4869
+ return this.emitQueue;
4870
+ }
4871
+ /** Tear down the tavern presence cleanly. */
4872
+ async stop(reason = "cli_exit") {
4873
+ if (this.stopped) return;
4874
+ this.stopped = true;
4875
+ if (this.reconnectTimer) {
4876
+ clearTimeout(this.reconnectTimer);
4877
+ this.reconnectTimer = null;
4878
+ }
4879
+ if (this.announceRetryTimer) {
4880
+ clearTimeout(this.announceRetryTimer);
4881
+ this.announceRetryTimer = null;
4882
+ }
4883
+ if (this.ws) {
4884
+ try {
4885
+ this.ws.close();
4886
+ } catch {}
4887
+ this.ws = null;
4888
+ }
4889
+ if (this.config && this.instanceId) await this.post("/disconnect", {
4890
+ instanceId: this.instanceId,
4891
+ reason
4892
+ }).catch(() => {});
4893
+ }
4894
+ async announce(body) {
4895
+ try {
4896
+ await this.post("/announce", body);
4897
+ return true;
4898
+ } catch (err) {
4899
+ logger.info(`[tavern] bridge announce failed: ${err.message}`);
4900
+ return false;
4901
+ }
4902
+ }
4903
+ async post(path, body) {
4904
+ if (!this.config) throw new Error("bridge config not loaded");
4905
+ const url = `http://127.0.0.1:${this.config.port ?? DEFAULT_PORT}${path}?secret=${encodeURIComponent(this.config.hookSecret)}`;
4906
+ const res = await fetch(url, {
4907
+ method: "POST",
4908
+ headers: { "Content-Type": "application/json" },
4909
+ body: JSON.stringify(body),
4910
+ signal: AbortSignal.timeout(ANNOUNCE_TIMEOUT_MS)
4911
+ });
4912
+ if (!res.ok) throw new Error(`bridge ${path} -> ${res.status}`);
4913
+ }
4914
+ connectCommandWs() {
4915
+ if (this.stopped || !this.config || !this.instanceId) return;
4916
+ const url = `ws://127.0.0.1:${this.config.port ?? DEFAULT_PORT}/commands?instanceId=${encodeURIComponent(this.instanceId)}&secret=${encodeURIComponent(this.config.hookSecret)}`;
4917
+ let ws;
4918
+ try {
4919
+ ws = new WsWebSocket(url);
4920
+ } catch (err) {
4921
+ logger.debug(`[tavern] command WS construct failed: ${err.message}`);
4922
+ this.scheduleReconnect();
4923
+ return;
4924
+ }
4925
+ this.ws = ws;
4926
+ ws.on("open", () => {
4927
+ this.reconnectAttempts = 0;
4928
+ logger.debug("[tavern] command WS open");
4929
+ });
4930
+ ws.on("message", (raw) => {
4931
+ let frame = null;
4932
+ try {
4933
+ frame = JSON.parse(raw.toString());
4934
+ } catch {
4935
+ logger.debug("[tavern] malformed command frame; ignored");
4936
+ return;
4937
+ }
4938
+ if (!frame?.command) return;
4939
+ this.dispatchCommand(frame.command).catch((err) => logger.warn(`[tavern] command dispatch threw: ${err.message}`));
4940
+ });
4941
+ ws.on("close", () => {
4942
+ this.ws = null;
4943
+ if (this.stopped) return;
4944
+ logger.debug("[tavern] command WS closed; reconnecting");
4945
+ this.scheduleReconnect();
4946
+ });
4947
+ ws.on("error", (err) => {
4948
+ logger.debug(`[tavern] command WS error: ${err.message}`);
4949
+ });
4950
+ }
4951
+ scheduleReconnect() {
4952
+ if (this.stopped || this.reconnectTimer) return;
4953
+ this.reconnectAttempts += 1;
4954
+ const delay = Math.min(500 * 2 ** (this.reconnectAttempts - 1), 1e4);
4955
+ this.reconnectTimer = setTimeout(() => {
4956
+ this.reconnectTimer = null;
4957
+ this.connectCommandWs();
4958
+ }, delay);
4959
+ }
4960
+ async dispatchCommand(command) {
4961
+ switch (command.type) {
4962
+ case "send_prompt":
4963
+ if (this.callbacks.onSendPrompt) await this.callbacks.onSendPrompt(command.text);
4964
+ break;
4965
+ case "resolve_permission":
4966
+ if (this.callbacks.onResolvePermission) await this.callbacks.onResolvePermission(command.requestId, command.allow);
4967
+ break;
4968
+ case "abort":
4969
+ if (this.callbacks.onAbort) await this.callbacks.onAbort();
4970
+ break;
4971
+ }
4972
+ }
4973
+ };
4974
+ /** Process-wide singleton — the CLI only ever has one tavern presence per run. */
4975
+ const bridgePresence = new BridgePresence();
4976
+ //#endregion
4314
4977
  //#region src/index.tsx
4315
4978
  process.removeAllListeners("warning");
4316
4979
  process.on("warning", (warning) => {
@@ -4326,6 +4989,16 @@ function getRequiredJupyterClient() {
4326
4989
  if (!client) throw new Error("Jupyter not configured. Set JUPYTER_SERVER_URL and optionally JUPYTER_TOKEN environment variables.");
4327
4990
  return client;
4328
4991
  }
4992
+ /**
4993
+ * Render the first question from a UserQuestion payload as a 240-char-capped
4994
+ * summary for tavern status events. Returns undefined if the payload has no
4995
+ * questions (defensive: payload.questions[0]?.question is typed string but
4996
+ * the array itself can be empty).
4997
+ */
4998
+ function summarizeUserQuestion(payload) {
4999
+ const first = payload.questions?.[0]?.question;
5000
+ return first ? first.slice(0, 240) : void 0;
5001
+ }
4329
5002
  let exitTimestamp = null;
4330
5003
  const EXIT_TIMEOUT_MS = 2e3;
4331
5004
  let usageCache = null;
@@ -4365,7 +5038,6 @@ function CliApp() {
4365
5038
  const imageStoreInitPromise = useRef(null);
4366
5039
  const setStoreSession = useCliStore((state) => state.setSession);
4367
5040
  const enqueuePermissionPrompt = useCliStore((state) => state.enqueuePermissionPrompt);
4368
- const dequeuePermissionPrompt = useCliStore((state) => state.dequeuePermissionPrompt);
4369
5041
  const enqueueUserQuestionPrompt = useCliStore((state) => state.enqueueUserQuestionPrompt);
4370
5042
  const dequeueUserQuestionPrompt = useCliStore((state) => state.dequeueUserQuestionPrompt);
4371
5043
  const setShowConfigEditor = useCliStore((state) => state.setShowConfigEditor);
@@ -4384,6 +5056,9 @@ function CliApp() {
4384
5056
  state.wsManager.disconnect();
4385
5057
  setWebSocketToolExecutor(null);
4386
5058
  }
5059
+ cleanupTasks.push(bridgePresence.stop("cli_exit").catch((err) => {
5060
+ logger.debug(`[CLEANUP] Bridge presence stop error: ${err.message}`);
5061
+ }));
4387
5062
  if (state.agent) state.agent.removeAllListeners();
4388
5063
  if (state.imageStore) try {
4389
5064
  state.imageStore.close();
@@ -4486,10 +5161,12 @@ function CliApp() {
4486
5161
  };
4487
5162
  let wsManager = null;
4488
5163
  let llm;
5164
+ let completionsUrl;
4489
5165
  try {
4490
5166
  const serverConfig = await apiClient.get("/api/settings/serverConfig");
4491
5167
  const wsUrl = serverConfig?.websocketUrl;
4492
5168
  const wsCompletionUrl = serverConfig?.wsCompletionUrl;
5169
+ completionsUrl = serverConfig?.completionsUrl;
4493
5170
  if (wsUrl && wsCompletionUrl) {
4494
5171
  wsManager = new WebSocketConnectionManager(wsUrl, tokenGetter);
4495
5172
  await wsManager.connect();
@@ -4605,7 +5282,8 @@ function CliApp() {
4605
5282
  setWebSocketToolExecutor(null);
4606
5283
  llm = new ServerLlmBackend({
4607
5284
  apiClient,
4608
- model: config.defaultModel
5285
+ model: config.defaultModel,
5286
+ completionsUrl
4609
5287
  });
4610
5288
  }
4611
5289
  const ollamaHost = process.env.B4M_OLLAMA_HOST;
@@ -4700,8 +5378,9 @@ function CliApp() {
4700
5378
  const promptFn = (toolName, args, preview) => {
4701
5379
  return new Promise((resolve) => {
4702
5380
  const canBeTrusted = permissionManager.canBeTrusted(toolName);
5381
+ const id = `perm-${++permissionPromptCounter}`;
4703
5382
  const prompt = {
4704
- id: `perm-${++permissionPromptCounter}`,
5383
+ id,
4705
5384
  toolName,
4706
5385
  args,
4707
5386
  preview,
@@ -4713,6 +5392,24 @@ function CliApp() {
4713
5392
  permissionManager
4714
5393
  }));
4715
5394
  enqueuePermissionPrompt(prompt);
5395
+ let summary;
5396
+ if (preview) summary = preview.slice(0, 4e3);
5397
+ else try {
5398
+ summary = JSON.stringify(args).slice(0, 4e3);
5399
+ } catch {
5400
+ summary = void 0;
5401
+ }
5402
+ bridgePresence.emitEvent({
5403
+ type: "permission_request",
5404
+ requestId: id,
5405
+ toolName,
5406
+ input: summary
5407
+ });
5408
+ bridgePresence.emitEvent({
5409
+ type: "status",
5410
+ status: "awaiting_permission",
5411
+ text: `${toolName} permission requested`.slice(0, 240)
5412
+ });
4716
5413
  });
4717
5414
  };
4718
5415
  let userQuestionCounter = 0;
@@ -4723,6 +5420,11 @@ function CliApp() {
4723
5420
  payload,
4724
5421
  resolve
4725
5422
  });
5423
+ bridgePresence.emitEvent({
5424
+ type: "status",
5425
+ status: "awaiting_input",
5426
+ text: summarizeUserQuestion(payload)
5427
+ });
4726
5428
  });
4727
5429
  };
4728
5430
  const agentContext = {
@@ -4878,6 +5580,82 @@ function CliApp() {
4878
5580
  subagent.off("observation", stepHandler);
4879
5581
  subagent.off("action", stepHandler);
4880
5582
  });
5583
+ const pendingToolUseIds = /* @__PURE__ */ new Map();
5584
+ const summarizeToolInput = (input) => {
5585
+ if (input == null) return void 0;
5586
+ if (typeof input === "string") return input.slice(0, 240);
5587
+ try {
5588
+ return JSON.stringify(input).slice(0, 240);
5589
+ } catch {
5590
+ return;
5591
+ }
5592
+ };
5593
+ const tavernActionHandler = (step) => {
5594
+ const toolName = step.metadata?.toolName ?? "tool";
5595
+ const toolUseId = `${toolName}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5596
+ const queue = pendingToolUseIds.get(toolName) ?? [];
5597
+ queue.push(toolUseId);
5598
+ pendingToolUseIds.set(toolName, queue);
5599
+ bridgePresence.emitEvent({
5600
+ type: "tool_use",
5601
+ tool: toolName,
5602
+ toolUseId,
5603
+ text: summarizeToolInput(step.metadata?.toolInput)
5604
+ });
5605
+ };
5606
+ const tavernObservationHandler = (step) => {
5607
+ const toolName = step.metadata?.toolName;
5608
+ let toolUseId;
5609
+ if (toolName) {
5610
+ const queue = pendingToolUseIds.get(toolName);
5611
+ if (queue && queue.length > 0) {
5612
+ toolUseId = queue.shift();
5613
+ if (queue.length === 0) pendingToolUseIds.delete(toolName);
5614
+ }
5615
+ }
5616
+ if (!toolUseId) {
5617
+ for (const [name, queue] of pendingToolUseIds.entries()) if (queue.length > 0) {
5618
+ toolUseId = queue.shift();
5619
+ if (queue.length === 0) pendingToolUseIds.delete(name);
5620
+ break;
5621
+ }
5622
+ }
5623
+ if (!toolUseId) toolUseId = `orphan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5624
+ bridgePresence.emitEvent({
5625
+ type: "tool_result",
5626
+ tool: toolName,
5627
+ toolUseId,
5628
+ text: typeof step.content === "string" ? step.content.slice(0, 4e3) : void 0
5629
+ });
5630
+ };
5631
+ const tavernFinalAnswerHandler = (step) => {
5632
+ const text = typeof step.content === "string" ? step.content : "";
5633
+ if (!text) return;
5634
+ bridgePresence.emitEvent({
5635
+ type: "message",
5636
+ role: "assistant",
5637
+ text: text.slice(0, 4e3)
5638
+ });
5639
+ };
5640
+ agent.on("action", tavernActionHandler);
5641
+ agent.on("observation", tavernObservationHandler);
5642
+ agent.on("final_answer", tavernFinalAnswerHandler);
5643
+ orchestrator.setBeforeRunCallback((subagent, _subagentType) => {
5644
+ subagent.on("thought", stepHandler);
5645
+ subagent.on("observation", stepHandler);
5646
+ subagent.on("action", stepHandler);
5647
+ subagent.on("action", tavernActionHandler);
5648
+ subagent.on("observation", tavernObservationHandler);
5649
+ subagent.on("final_answer", tavernFinalAnswerHandler);
5650
+ });
5651
+ orchestrator.setAfterRunCallback((subagent, _subagentType) => {
5652
+ subagent.off("thought", stepHandler);
5653
+ subagent.off("observation", stepHandler);
5654
+ subagent.off("action", stepHandler);
5655
+ subagent.off("action", tavernActionHandler);
5656
+ subagent.off("observation", tavernObservationHandler);
5657
+ subagent.off("final_answer", tavernFinalAnswerHandler);
5658
+ });
4881
5659
  setState((prev) => ({
4882
5660
  ...prev,
4883
5661
  session: newSession,
@@ -4969,6 +5747,60 @@ function CliApp() {
4969
5747
  useEffect(() => {
4970
5748
  init();
4971
5749
  }, [init]);
5750
+ const handleMessageRef = useRef(null);
5751
+ const abortControllerRef = useRef(state.abortController);
5752
+ abortControllerRef.current = state.abortController;
5753
+ const emitNextAwaitingStatus = () => {
5754
+ const s = useCliStore.getState();
5755
+ if (s.permissionPrompt) {
5756
+ bridgePresence.emitEvent({
5757
+ type: "status",
5758
+ status: "awaiting_permission",
5759
+ text: `${s.permissionPrompt.toolName} permission requested`.slice(0, 240)
5760
+ });
5761
+ return;
5762
+ }
5763
+ if (s.userQuestionPrompt) {
5764
+ bridgePresence.emitEvent({
5765
+ type: "status",
5766
+ status: "awaiting_input",
5767
+ text: summarizeUserQuestion(s.userQuestionPrompt.payload)
5768
+ });
5769
+ return;
5770
+ }
5771
+ const abort = abortControllerRef.current;
5772
+ if (!abort || abort.signal.aborted) return;
5773
+ bridgePresence.emitEvent({
5774
+ type: "status",
5775
+ status: "running"
5776
+ });
5777
+ };
5778
+ useEffect(() => {
5779
+ if (!isInitialized) return;
5780
+ let cancelled = false;
5781
+ bridgePresence.setCallbacks({
5782
+ onSendPrompt: (text) => handleMessageRef.current?.(text),
5783
+ onAbort: () => abortControllerRef.current?.abort(),
5784
+ onResolvePermission: (requestId, allow) => {
5785
+ const action = allow ? "allow-once" : "deny";
5786
+ if (!useCliStore.getState().resolvePermissionPromptById(requestId, action)) return;
5787
+ bridgePresence.emitEvent({
5788
+ type: "permission_resolved",
5789
+ requestId,
5790
+ allow,
5791
+ resolvedBy: "user"
5792
+ });
5793
+ emitNextAwaitingStatus();
5794
+ }
5795
+ });
5796
+ bridgePresence.start({ workspacePath: process.cwd() }).then((live) => {
5797
+ if (cancelled) return;
5798
+ if (live) logger.debug("[tavern] presence active");
5799
+ }).catch((err) => logger.debug(`[tavern] start threw: ${err.message}`));
5800
+ return () => {
5801
+ cancelled = true;
5802
+ };
5803
+ }, [isInitialized]);
4972
5804
  /**
4973
5805
  * Handle custom command execution with proper display
4974
5806
  * Shows concise user message but sends full template to agent
@@ -5188,6 +6020,16 @@ function CliApp() {
5188
6020
  console.error("❌ CLI failed to initialize. Try restarting b4m.\n");
5189
6021
  return;
5190
6022
  }
6023
+ bridgePresence.emitEvent({
6024
+ type: "message",
6025
+ role: "user",
6026
+ text: message.slice(0, 4e3)
6027
+ });
6028
+ bridgePresence.emitEvent({
6029
+ type: "status",
6030
+ status: "running",
6031
+ text: message.slice(0, 240)
6032
+ });
5191
6033
  await state.commandHistoryStore.add(message);
5192
6034
  setCommandHistory(await state.commandHistoryStore.list());
5193
6035
  const config = state.config;
@@ -5358,13 +6200,19 @@ function CliApp() {
5358
6200
  console.error(`\n❌ ${errorMessage}\n`);
5359
6201
  logger.debug(`Full error details: ${error instanceof Error ? error.stack || error.message : String(error)}`);
5360
6202
  } finally {
6203
+ const wasAborted = abortController.signal.aborted;
5361
6204
  setState((prev) => ({
5362
6205
  ...prev,
5363
6206
  abortController: null
5364
6207
  }));
5365
6208
  useCliStore.getState().setIsThinking(false);
6209
+ bridgePresence.emitEvent({
6210
+ type: "status",
6211
+ status: wasAborted ? "idle" : "awaiting_input"
6212
+ });
5366
6213
  }
5367
6214
  };
6215
+ handleMessageRef.current = handleMessage;
5368
6216
  /**
5369
6217
  * Handle background agent completion - runs agent to process results silently
5370
6218
  * without adding a user message to the conversation.
@@ -6857,19 +7705,22 @@ Multi-line Input:
6857
7705
  }));
6858
7706
  },
6859
7707
  mcpManager: state.mcpManager ?? void 0,
6860
- onPermissionResponse: (response) => {
6861
- const currentPrompt = useCliStore.getState().permissionPrompt;
6862
- if (currentPrompt) {
6863
- currentPrompt.resolve({ action: response });
6864
- dequeuePermissionPrompt();
6865
- }
7708
+ onPermissionResponse: (response, promptId) => {
7709
+ if (!useCliStore.getState().resolvePermissionPromptById(promptId, response)) return;
7710
+ bridgePresence.emitEvent({
7711
+ type: "permission_resolved",
7712
+ requestId: promptId,
7713
+ allow: response !== "deny",
7714
+ resolvedBy: "user"
7715
+ });
7716
+ emitNextAwaitingStatus();
6866
7717
  },
6867
- onUserQuestionResponse: (response) => {
7718
+ onUserQuestionResponse: (response, promptId) => {
6868
7719
  const currentPrompt = useCliStore.getState().userQuestionPrompt;
6869
- if (currentPrompt) {
6870
- currentPrompt.resolve(response);
6871
- dequeueUserQuestionPrompt();
6872
- }
7720
+ if (currentPrompt?.id !== promptId) return;
7721
+ currentPrompt.resolve(response);
7722
+ dequeueUserQuestionPrompt();
7723
+ emitNextAwaitingStatus();
6873
7724
  }
6874
7725
  });
6875
7726
  }