@tiflis-io/tiflis-code-workstation 0.3.7 → 0.3.9

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.
Files changed (2) hide show
  1. package/dist/main.js +394 -126
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -247,18 +247,18 @@ function getProtocolVersion() {
247
247
  return `${PROTOCOL_VERSION.major}.${PROTOCOL_VERSION.minor}.${PROTOCOL_VERSION.patch}`;
248
248
  }
249
249
  var CONNECTION_TIMING = {
250
- /** How often to send ping to tunnel (15 seconds - keeps connection alive through proxies) */
251
- PING_INTERVAL_MS: 15e3,
252
- /** Max time to wait for pong before considering connection stale (30 seconds) */
253
- PONG_TIMEOUT_MS: 3e4,
254
- /** Max time to wait for registration response (15 seconds) */
255
- REGISTRATION_TIMEOUT_MS: 15e3,
256
- /** Minimum reconnect delay (1 second) */
257
- RECONNECT_DELAY_MIN_MS: 1e3,
258
- /** Maximum reconnect delay (30 seconds) */
259
- RECONNECT_DELAY_MAX_MS: 3e4,
260
- /** Interval for checking timed-out client connections (10 seconds) */
261
- CLIENT_TIMEOUT_CHECK_INTERVAL_MS: 1e4
250
+ /** How often to send ping to tunnel (5 seconds - fast liveness detection) */
251
+ PING_INTERVAL_MS: 5e3,
252
+ /** Max time to wait for pong before considering connection stale (10 seconds) */
253
+ PONG_TIMEOUT_MS: 1e4,
254
+ /** Max time to wait for registration response (10 seconds) */
255
+ REGISTRATION_TIMEOUT_MS: 1e4,
256
+ /** Minimum reconnect delay (500ms - fast first retry) */
257
+ RECONNECT_DELAY_MIN_MS: 500,
258
+ /** Maximum reconnect delay (5 seconds - don't wait too long) */
259
+ RECONNECT_DELAY_MAX_MS: 5e3,
260
+ /** Interval for checking timed-out client connections (5 seconds - faster cleanup) */
261
+ CLIENT_TIMEOUT_CHECK_INTERVAL_MS: 5e3
262
262
  };
263
263
  var SESSION_CONFIG = {
264
264
  /** Maximum number of concurrent agent sessions */
@@ -1895,13 +1895,37 @@ import { join as join4 } from "path";
1895
1895
  import { execSync } from "child_process";
1896
1896
  var FileSystemWorkspaceDiscovery = class {
1897
1897
  workspacesRoot;
1898
+ cacheTtlMs;
1899
+ /** Cache for workspace list */
1900
+ workspacesCache = null;
1901
+ /** Cache for projects by workspace name */
1902
+ projectsCache = /* @__PURE__ */ new Map();
1898
1903
  constructor(config2) {
1899
1904
  this.workspacesRoot = config2.workspacesRoot;
1905
+ this.cacheTtlMs = config2.cacheTtlMs ?? 3e4;
1906
+ }
1907
+ /**
1908
+ * Checks if a cache entry is still valid.
1909
+ */
1910
+ isCacheValid(entry) {
1911
+ if (!entry) return false;
1912
+ return Date.now() - entry.timestamp < this.cacheTtlMs;
1913
+ }
1914
+ /**
1915
+ * Invalidates all caches. Call when workspace structure changes.
1916
+ */
1917
+ invalidateCache() {
1918
+ this.workspacesCache = null;
1919
+ this.projectsCache.clear();
1900
1920
  }
1901
1921
  /**
1902
1922
  * Lists all workspaces in the workspaces root.
1923
+ * Results are cached for faster subsequent calls.
1903
1924
  */
1904
1925
  async listWorkspaces() {
1926
+ if (this.isCacheValid(this.workspacesCache)) {
1927
+ return this.workspacesCache.data;
1928
+ }
1905
1929
  const entries = await readdir(this.workspacesRoot, { withFileTypes: true });
1906
1930
  const workspaces = [];
1907
1931
  for (const entry of entries) {
@@ -1915,12 +1939,22 @@ var FileSystemWorkspaceDiscovery = class {
1915
1939
  });
1916
1940
  }
1917
1941
  }
1918
- return workspaces.sort((a, b) => a.name.localeCompare(b.name));
1942
+ const result = workspaces.sort((a, b) => a.name.localeCompare(b.name));
1943
+ this.workspacesCache = {
1944
+ data: result,
1945
+ timestamp: Date.now()
1946
+ };
1947
+ return result;
1919
1948
  }
1920
1949
  /**
1921
1950
  * Lists all projects in a workspace.
1951
+ * Results are cached for faster subsequent calls.
1922
1952
  */
1923
1953
  async listProjects(workspace) {
1954
+ const cached = this.projectsCache.get(workspace);
1955
+ if (this.isCacheValid(cached)) {
1956
+ return cached.data;
1957
+ }
1924
1958
  const workspacePath = join4(this.workspacesRoot, workspace);
1925
1959
  if (!await this.pathExists(workspacePath)) {
1926
1960
  return [];
@@ -1949,7 +1983,12 @@ var FileSystemWorkspaceDiscovery = class {
1949
1983
  });
1950
1984
  }
1951
1985
  }
1952
- return projects.sort((a, b) => a.name.localeCompare(b.name));
1986
+ const result = projects.sort((a, b) => a.name.localeCompare(b.name));
1987
+ this.projectsCache.set(workspace, {
1988
+ data: result,
1989
+ timestamp: Date.now()
1990
+ });
1991
+ return result;
1953
1992
  }
1954
1993
  /**
1955
1994
  * Gets information about a specific project.
@@ -3141,6 +3180,12 @@ var HeadlessAgentExecutor = class extends EventEmitter {
3141
3180
 
3142
3181
  // src/domain/value-objects/content-block.ts
3143
3182
  import { randomUUID } from "crypto";
3183
+ function isTextBlock(block) {
3184
+ return block.block_type === "text";
3185
+ }
3186
+ function isToolBlock(block) {
3187
+ return block.block_type === "tool";
3188
+ }
3144
3189
  function createTextBlock(content) {
3145
3190
  return {
3146
3191
  id: randomUUID(),
@@ -3219,6 +3264,103 @@ function createVoiceOutputBlock(text2, options) {
3219
3264
  }
3220
3265
  };
3221
3266
  }
3267
+ function mergeToolBlocks(blocks) {
3268
+ const toolBlocksByUseId = /* @__PURE__ */ new Map();
3269
+ for (const block of blocks) {
3270
+ if (isToolBlock(block) && block.metadata.tool_use_id) {
3271
+ const toolUseId = block.metadata.tool_use_id;
3272
+ const existing = toolBlocksByUseId.get(toolUseId);
3273
+ if (existing) {
3274
+ const mergedStatus = getMergedToolStatus(
3275
+ existing.metadata.tool_status,
3276
+ block.metadata.tool_status
3277
+ );
3278
+ const mergedBlock = {
3279
+ id: existing.id,
3280
+ // Keep original ID
3281
+ block_type: "tool",
3282
+ content: block.metadata.tool_name || existing.content,
3283
+ metadata: {
3284
+ tool_name: block.metadata.tool_name || existing.metadata.tool_name,
3285
+ tool_use_id: toolUseId,
3286
+ tool_input: block.metadata.tool_input || existing.metadata.tool_input,
3287
+ tool_output: block.metadata.tool_output || existing.metadata.tool_output,
3288
+ tool_status: mergedStatus
3289
+ }
3290
+ };
3291
+ toolBlocksByUseId.set(toolUseId, mergedBlock);
3292
+ } else {
3293
+ toolBlocksByUseId.set(toolUseId, block);
3294
+ }
3295
+ }
3296
+ }
3297
+ const seenToolUseIds = /* @__PURE__ */ new Set();
3298
+ const result = [];
3299
+ for (const block of blocks) {
3300
+ if (isToolBlock(block) && block.metadata.tool_use_id) {
3301
+ const toolUseId = block.metadata.tool_use_id;
3302
+ if (!seenToolUseIds.has(toolUseId)) {
3303
+ seenToolUseIds.add(toolUseId);
3304
+ const mergedBlock = toolBlocksByUseId.get(toolUseId);
3305
+ if (mergedBlock) {
3306
+ result.push(mergedBlock);
3307
+ }
3308
+ }
3309
+ } else {
3310
+ result.push(block);
3311
+ }
3312
+ }
3313
+ return result;
3314
+ }
3315
+ function getMergedToolStatus(status1, status2) {
3316
+ if (status1 === "completed" || status2 === "completed") {
3317
+ return "completed";
3318
+ }
3319
+ if (status1 === "failed" || status2 === "failed") {
3320
+ return "failed";
3321
+ }
3322
+ return "running";
3323
+ }
3324
+ function accumulateBlocks(existing, newBlocks) {
3325
+ for (const block of newBlocks) {
3326
+ if (isToolBlock(block) && block.metadata.tool_use_id) {
3327
+ const toolUseId = block.metadata.tool_use_id;
3328
+ const existingIndex = existing.findIndex(
3329
+ (b) => isToolBlock(b) && b.metadata.tool_use_id === toolUseId
3330
+ );
3331
+ if (existingIndex >= 0) {
3332
+ const existingBlock = existing[existingIndex];
3333
+ const mergedStatus = getMergedToolStatus(
3334
+ existingBlock.metadata.tool_status,
3335
+ block.metadata.tool_status
3336
+ );
3337
+ existing[existingIndex] = {
3338
+ id: existingBlock.id,
3339
+ block_type: "tool",
3340
+ content: block.metadata.tool_name || existingBlock.content,
3341
+ metadata: {
3342
+ tool_name: block.metadata.tool_name || existingBlock.metadata.tool_name,
3343
+ tool_use_id: toolUseId,
3344
+ tool_input: block.metadata.tool_input || existingBlock.metadata.tool_input,
3345
+ tool_output: block.metadata.tool_output || existingBlock.metadata.tool_output,
3346
+ tool_status: mergedStatus
3347
+ }
3348
+ };
3349
+ } else {
3350
+ existing.push(block);
3351
+ }
3352
+ } else if (isTextBlock(block)) {
3353
+ const lastBlock = existing[existing.length - 1];
3354
+ if (lastBlock && isTextBlock(lastBlock)) {
3355
+ existing[existing.length - 1] = block;
3356
+ } else {
3357
+ existing.push(block);
3358
+ }
3359
+ } else {
3360
+ existing.push(block);
3361
+ }
3362
+ }
3363
+ }
3222
3364
 
3223
3365
  // src/infrastructure/agents/agent-output-parser.ts
3224
3366
  var AgentOutputParser = class _AgentOutputParser {
@@ -5598,7 +5740,7 @@ var ChatHistoryService = class _ChatHistoryService {
5598
5740
  * Gets supervisor chat history (global, shared across all devices).
5599
5741
  * Returns messages sorted by sequence (oldest first) for chronological display.
5600
5742
  */
5601
- getSupervisorHistory(limit = 50) {
5743
+ getSupervisorHistory(limit = 20) {
5602
5744
  const sessionId = _ChatHistoryService.SUPERVISOR_SESSION_ID;
5603
5745
  const rows = this.messageRepo.getBySession(sessionId, limit);
5604
5746
  return rows.reverse().map((row) => {
@@ -5663,7 +5805,7 @@ var ChatHistoryService = class _ChatHistoryService {
5663
5805
  * Gets agent session chat history.
5664
5806
  * Returns messages sorted chronologically (oldest first).
5665
5807
  */
5666
- getAgentHistory(sessionId, limit = 100) {
5808
+ getAgentHistory(sessionId, limit = 20) {
5667
5809
  const rows = this.messageRepo.getBySession(sessionId, limit);
5668
5810
  return rows.reverse().map((row) => {
5669
5811
  let contentBlocks;
@@ -5721,7 +5863,7 @@ var ChatHistoryService = class _ChatHistoryService {
5721
5863
  * @param sessionIds - List of active agent session IDs
5722
5864
  * @param limit - Max messages per session
5723
5865
  */
5724
- getAllAgentHistories(sessionIds, limit = 50) {
5866
+ getAllAgentHistories(sessionIds, limit = 20) {
5725
5867
  const histories = /* @__PURE__ */ new Map();
5726
5868
  for (const sessionId of sessionIds) {
5727
5869
  const history = this.getAgentHistory(sessionId, limit);
@@ -7346,12 +7488,7 @@ var SupervisorAgent = class extends EventEmitter4 {
7346
7488
  if (this.isExecuting && !this.isCancelled) {
7347
7489
  const textBlock = createTextBlock(content);
7348
7490
  this.emit("blocks", deviceId, [textBlock], false);
7349
- const lastTextIndex = allBlocks.findLastIndex((b) => b.block_type === "text");
7350
- if (lastTextIndex >= 0) {
7351
- allBlocks[lastTextIndex] = textBlock;
7352
- } else {
7353
- allBlocks.push(textBlock);
7354
- }
7491
+ accumulateBlocks(allBlocks, [textBlock]);
7355
7492
  }
7356
7493
  } else if (Array.isArray(content)) {
7357
7494
  for (const item of content) {
@@ -7359,7 +7496,7 @@ var SupervisorAgent = class extends EventEmitter4 {
7359
7496
  const block = this.parseContentItem(item);
7360
7497
  if (block) {
7361
7498
  this.emit("blocks", deviceId, [block], false);
7362
- allBlocks.push(block);
7499
+ accumulateBlocks(allBlocks, [block]);
7363
7500
  }
7364
7501
  }
7365
7502
  }
@@ -7368,21 +7505,24 @@ var SupervisorAgent = class extends EventEmitter4 {
7368
7505
  if (lastMessage.getType() === "tool" && this.isExecuting && !this.isCancelled) {
7369
7506
  const toolContent = lastMessage.content;
7370
7507
  const toolName = lastMessage.name ?? "tool";
7508
+ const toolCallId = lastMessage.tool_call_id;
7371
7509
  const toolBlock = createToolBlock(
7372
7510
  toolName,
7373
7511
  "completed",
7374
7512
  void 0,
7375
- typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent)
7513
+ typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent),
7514
+ toolCallId
7376
7515
  );
7377
7516
  this.emit("blocks", deviceId, [toolBlock], false);
7378
- allBlocks.push(toolBlock);
7517
+ accumulateBlocks(allBlocks, [toolBlock]);
7379
7518
  }
7380
7519
  }
7381
7520
  if (this.isExecuting && !this.isCancelled) {
7382
7521
  this.addToHistory("user", command);
7383
7522
  this.addToHistory("assistant", finalOutput);
7523
+ const finalBlocks = mergeToolBlocks(allBlocks);
7384
7524
  const completionBlock = createStatusBlock("Complete");
7385
- this.emit("blocks", deviceId, [completionBlock], true, finalOutput, allBlocks);
7525
+ this.emit("blocks", deviceId, [completionBlock], true, finalOutput, finalBlocks);
7386
7526
  this.logger.debug({ output: finalOutput.slice(0, 200) }, "Supervisor streaming completed");
7387
7527
  } else {
7388
7528
  this.logger.info({ deviceId, isCancelled: this.isCancelled, isExecuting: this.isExecuting }, "Supervisor streaming ended due to cancellation");
@@ -7412,7 +7552,8 @@ var SupervisorAgent = class extends EventEmitter4 {
7412
7552
  if (type === "tool_use") {
7413
7553
  const name = typeof item.name === "string" ? item.name : "tool";
7414
7554
  const input = item.input;
7415
- return createToolBlock(name, "running", input);
7555
+ const toolUseId = typeof item.id === "string" ? item.id : void 0;
7556
+ return createToolBlock(name, "running", input, void 0, toolUseId);
7416
7557
  }
7417
7558
  return null;
7418
7559
  }
@@ -7508,107 +7649,179 @@ var SupervisorAgent = class extends EventEmitter4 {
7508
7649
  * Builds the system message for the agent.
7509
7650
  */
7510
7651
  buildSystemMessage() {
7511
- const systemPrompt = `You are the Supervisor Agent for Tiflis Code, a workstation management system.
7512
-
7513
- Your role is to help users:
7514
- 1. **Discover workspaces and projects** - List available workspaces and projects
7515
- 2. **Manage git worktrees** - Create, list, and remove worktrees for parallel development
7516
- 3. **Manage sessions** - Create and terminate agent sessions (Cursor, Claude, OpenCode) and terminal sessions
7517
- 4. **Navigate the file system** - List directories and read files
7518
- 5. **Complete feature workflows** - Merge branches, clean up worktrees, and manage related sessions
7652
+ const systemPrompt = `## MANDATORY RULES (STRICTLY ENFORCED)
7519
7653
 
7520
- ## CRITICAL: Always Use Tools - Never Be Lazy
7654
+ You MUST always respond in English.
7521
7655
 
7522
- **YOU MUST ALWAYS call tools to execute user requests. NEVER be lazy or skip actions based on memory or previous context.**
7656
+ You MUST ALWAYS call tools to execute user requests. You MUST NEVER skip actions based on memory or previous context.
7523
7657
 
7524
- ### Mandatory Tool Usage Rules:
7658
+ ### Tool Usage Requirements:
7525
7659
 
7526
- 1. **ALWAYS call tools for fresh data** - When user asks about workspaces, projects, sessions, or any system state:
7527
- - ALWAYS call the appropriate tool (list_workspaces, list_projects, list_sessions, etc.)
7660
+ 1. You MUST call tools for fresh data on EVERY request:
7661
+ - ALWAYS call list_workspaces, list_projects, list_sessions, etc. when asked
7528
7662
  - NEVER respond from memory or previous conversation context
7529
- - System state changes constantly - what was true before may not be true now
7663
+ - System state changes constantly - previous data is stale
7530
7664
 
7531
- 2. **ALWAYS execute requested actions** - When user asks to create, terminate, or modify something:
7665
+ 2. You MUST execute requested actions immediately:
7532
7666
  - ALWAYS call the tool to perform the action, even if you think it was done before
7533
- - If user asks to create a session and one already exists, CREATE ANOTHER ONE (user knows what they want)
7534
- - If user asks to list projects, LIST THEM NOW with a tool call (don't say "I already showed you")
7535
- - NEVER refuse a direct request because "it was already done" or "nothing changed"
7536
-
7537
- 3. **User intent is paramount** - When user explicitly requests an action:
7538
- - Execute it immediately without questioning or suggesting alternatives
7539
- - Don't assume user made a mistake - they know what they need
7540
- - Multiple sessions in the same project is a valid use case
7667
+ - If user asks to create a session and one already exists, CREATE ANOTHER ONE
7668
+ - If user asks to list projects, LIST THEM NOW with a tool call
7669
+ - NEVER refuse a direct request because "it was already done"
7670
+
7671
+ 3. User intent is paramount:
7672
+ - Execute requests immediately without questioning
7673
+ - Do NOT assume user made a mistake
7674
+ - Multiple sessions in the same project is valid
7541
7675
  - Refreshing information is always valid
7542
7676
 
7543
- 4. **No shortcuts** - You must:
7544
- - Call list_workspaces/list_projects EVERY time user asks what workspaces/projects exist
7677
+ 4. Required tool calls:
7678
+ - Call list_workspaces/list_projects EVERY time user asks about workspaces/projects
7545
7679
  - Call list_sessions EVERY time user asks about active sessions
7680
+ - Call list_available_agents BEFORE creating any agent session - NEVER skip this step
7546
7681
  - Call create_agent_session/create_terminal_session EVERY time user asks to create a session
7547
- - Never say "based on our previous conversation" or "as I mentioned earlier" for factual data
7682
+ - NEVER say "based on our previous conversation" for factual data
7548
7683
 
7549
- ## Feature Completion & Merge Workflows
7684
+ 5. Agent selection (CRITICAL):
7685
+ - You MUST call list_available_agents BEFORE creating any agent session
7686
+ - Match user's requested agent name EXACTLY to available agents/aliases
7687
+ - If user says "open zai", use "zai" - do NOT substitute with base type like "claude"
7688
+ - If user request is ambiguous, show available agents and ask for clarification
7689
+ - NEVER assume which agent to use without checking list_available_agents first
7550
7690
 
7551
- When users ask to "complete the feature", "finish the work", "merge and clean up", or similar requests:
7691
+ ---
7552
7692
 
7553
- ### Safety Checks First:
7554
- 1. **Check branch status** with \`branch_status\` - Look for uncommitted changes
7555
- 2. **List active sessions** with \`get_worktree_session_summary\` - Find sessions in the worktree
7556
- 3. **Ask for confirmation** if there are uncommitted changes or active sessions
7693
+ ## YOUR ROLE
7557
7694
 
7558
- ### Complete Workflow with \`complete_feature\`:
7695
+ You are the Supervisor Agent for Tiflis Code, a workstation management system.
7696
+
7697
+ Your responsibilities:
7698
+ 1. Discover workspaces and projects - List available workspaces and projects
7699
+ 2. Manage git worktrees - Create, list, and remove worktrees for parallel development
7700
+ 3. Manage sessions - Create and terminate agent sessions (Cursor, Claude, OpenCode) and terminal sessions
7701
+ 4. Navigate the file system - List directories and read files
7702
+ 5. Complete feature workflows - Merge branches, clean up worktrees, and manage related sessions
7703
+
7704
+ ---
7705
+
7706
+ ## FEATURE COMPLETION WORKFLOW
7707
+
7708
+ When users ask to "complete the feature", "finish the work", or "merge and clean up":
7709
+
7710
+ Step 1: Check branch status with \`branch_status\` - Look for uncommitted changes
7711
+ Step 2: List active sessions with \`get_worktree_session_summary\` - Find sessions in the worktree
7712
+ Step 3: Ask for confirmation if there are uncommitted changes or active sessions
7713
+
7714
+ ### Complete Workflow Tool:
7715
+ Use \`complete_feature\` for one-command solution:
7559
7716
  - Merges feature branch into main with automatic push
7560
7717
  - Cleans up the worktree and removes the branch if merged
7561
- - One-command solution for feature completion
7562
7718
 
7563
7719
  ### Step-by-Step Alternative:
7564
- 1. **Handle uncommitted changes**: Commit, stash, or get user confirmation
7565
- 2. **Terminate sessions**: Use \`terminate_worktree_sessions\` to clean up active sessions
7566
- 3. **Merge branch**: Use \`merge_branch\` with pushAfter=true
7567
- 4. **Cleanup worktree**: Use \`cleanup_worktree\` to remove worktree directory
7720
+ Step 1: Handle uncommitted changes - Commit, stash, or get user confirmation
7721
+ Step 2: Terminate sessions - Use \`terminate_worktree_sessions\` to clean up active sessions
7722
+ Step 3: Merge branch - Use \`merge_branch\` with pushAfter=true
7723
+ Step 4: Cleanup worktree - Use \`cleanup_worktree\` to remove worktree directory
7568
7724
 
7569
7725
  ### Available Merge Tools:
7570
- - **branch_status** - Check current branch state and uncommitted changes
7571
- - **merge_branch** - Safe merge with conflict detection and push
7572
- - **complete_feature** - Full workflow (merge + cleanup + push)
7573
- - **cleanup_worktree** - Remove worktree and delete merged branch
7574
- - **list_mergeable_branches** - Show all branches and their cleanup eligibility
7575
- - **get_worktree_session_summary** - List sessions in a specific worktree
7576
- - **terminate_worktree_sessions** - End all sessions in a worktree
7726
+ - branch_status: Check current branch state and uncommitted changes
7727
+ - merge_branch: Safe merge with conflict detection and push
7728
+ - complete_feature: Full workflow (merge + cleanup + push)
7729
+ - cleanup_worktree: Remove worktree and delete merged branch
7730
+ - list_mergeable_branches: Show all branches and their cleanup eligibility
7731
+ - get_worktree_session_summary: List sessions in a specific worktree
7732
+ - terminate_worktree_sessions: End all sessions in a worktree
7577
7733
 
7578
7734
  ### Error Handling:
7579
- - **Merge conflicts**: Report conflicting files and suggest manual resolution
7580
- - **Uncommitted changes**: Offer to commit, stash, or force cleanup
7581
- - **Active sessions**: List sessions and ask for termination confirmation
7582
- - **Failed pushes**: Continue with local merge, warn about remote sync
7735
+ - Merge conflicts: Report conflicting files and suggest manual resolution
7736
+ - Uncommitted changes: Offer to commit, stash, or force cleanup
7737
+ - Active sessions: List sessions and ask for termination confirmation
7738
+ - Failed pushes: Continue with local merge, warn about remote sync
7583
7739
 
7584
- ## Guidelines:
7585
- - Be concise and helpful
7586
- - Use tools to gather information before responding
7587
- - When creating sessions, always confirm the workspace and project first
7588
- - For ambiguous requests, ask clarifying questions
7589
- - Format responses for terminal display (avoid markdown links)
7590
- - ALWAYS prioritize safety - check before deleting/merging
7740
+ ---
7741
+
7742
+ ## AGENT SELECTION (CRITICAL - FOLLOW STRICTLY)
7743
+
7744
+ When user asks to "open an agent", "start an agent", "create a session", or mentions any agent by name:
7745
+
7746
+ Step 1: You MUST call \`list_available_agents\` FIRST to get the current list of available agents and aliases
7747
+ Step 2: Match user intent to the correct agent from the list
7748
+ Step 3: Call \`create_agent_session\` with the exact agent name from the list
7749
+
7750
+ ### Agent Matching Rules:
7591
7751
 
7592
- ## Session Types:
7593
- - **cursor** - Cursor AI agent for code assistance
7594
- - **claude** - Claude Code CLI for AI coding
7595
- - **opencode** - OpenCode AI agent
7596
- - **terminal** - Shell terminal for direct commands
7752
+ 1. If user mentions a specific name (e.g., "open zai", "start claude", "use cursor"):
7753
+ - Find the EXACT match in the available agents list
7754
+ - If "zai" is an alias, use "zai" - do NOT substitute with the base type
7755
+ - If no exact match, suggest available options
7597
7756
 
7598
- ## Creating Agent Sessions:
7599
- When creating agent sessions, by default use the main project directory (main or master branch) unless the user explicitly requests a specific worktree or branch:
7600
- - **Default behavior**: Omit the \`worktree\` parameter to create session on the main/master branch (project root directory)
7601
- - **Specific worktree**: Only specify \`worktree\` when the user explicitly asks for a feature branch worktree (NOT the main branch)
7602
- - **IMPORTANT**: When \`list_worktrees\` shows a worktree named "main" with \`isMain: true\`, this represents the project root directory. Do NOT pass \`worktree: "main"\` - instead, omit the worktree parameter entirely to use the project root.
7757
+ 2. If user asks generically (e.g., "open an agent", "start a coding agent"):
7758
+ - Call \`list_available_agents\` and present the options
7759
+ - Ask user which agent they want to use
7760
+ - Do NOT pick the first one or make assumptions
7761
+
7762
+ 3. If user mentions a capability (e.g., "I need help with code review"):
7763
+ - Call \`list_available_agents\` to see descriptions
7764
+ - Match the capability to the agent description
7765
+ - If multiple agents match, ask user to choose
7766
+
7767
+ 4. NEVER skip \`list_available_agents\`:
7768
+ - Agent aliases are configured via environment variables
7769
+ - The list changes based on workstation configuration
7770
+ - You MUST always check what's actually available
7771
+
7772
+ ### Example Flow:
7773
+ User: "open zai on tiflis-code"
7774
+ Step 1: Call list_available_agents -> Returns: claude, cursor, opencode, zai (alias for claude)
7775
+ Step 2: User said "zai" -> Match found: "zai"
7776
+ Step 3: Call create_agent_session with agentName="zai"
7777
+
7778
+ ---
7779
+
7780
+ ## SESSION TYPES
7781
+
7782
+ Base agent types:
7783
+ - cursor: Cursor AI agent for code assistance
7784
+ - claude: Claude Code CLI for AI coding
7785
+ - opencode: OpenCode AI agent
7786
+ - terminal: Shell terminal for direct commands
7787
+
7788
+ Custom aliases: Configured via AGENT_ALIAS_* environment variables. Always call \`list_available_agents\` to see current aliases.
7789
+
7790
+ ### Creating Agent Sessions:
7791
+ Default: Omit the \`worktree\` parameter to create session on the main/master branch (project root directory)
7792
+ Specific worktree: Only specify \`worktree\` when user explicitly asks for a feature branch worktree (NOT the main branch)
7793
+ IMPORTANT: When \`list_worktrees\` shows a worktree named "main" with \`isMain: true\`, this represents the project root directory. Do NOT pass \`worktree: "main"\` - omit the worktree parameter entirely.
7794
+
7795
+ ---
7796
+
7797
+ ## WORKTREE MANAGEMENT
7603
7798
 
7604
- ## Worktree Management:
7605
7799
  Worktrees allow working on multiple branches simultaneously in separate directories.
7606
- - **Branch naming**: Use conventional format \`<type>/<name>\` where \`<name>\` is lower-kebab-case. Types: \`feature\`, \`fix\`, \`refactor\`, \`docs\`, \`chore\`. Examples: \`feature/user-auth\`, \`fix/keyboard-layout\`, \`refactor/websocket-handler\`
7607
- - **Directory pattern**: \`project--branch-name\` (slashes replaced with dashes, e.g., \`my-app--feature-user-auth\`)
7608
- - **Creating worktrees**: Use \`create_worktree\` tool with:
7609
- - \`createNewBranch: true\` \u2014 Creates a NEW branch and worktree (most common for new features)
7610
- - \`createNewBranch: false\` \u2014 Checks out an EXISTING branch into a worktree
7611
- - \`baseBranch\` \u2014 Optional starting point for new branches (defaults to HEAD, commonly "main")`;
7800
+
7801
+ Branch naming: Use conventional format \`<type>/<name>\` where \`<name>\` is lower-kebab-case
7802
+ Types: feature, fix, refactor, docs, chore
7803
+ Examples: feature/user-auth, fix/keyboard-layout, refactor/websocket-handler
7804
+
7805
+ Directory pattern: project--branch-name (slashes replaced with dashes, e.g., my-app--feature-user-auth)
7806
+
7807
+ Creating worktrees with \`create_worktree\`:
7808
+ - createNewBranch: true - Creates a NEW branch and worktree (most common for new features)
7809
+ - createNewBranch: false - Checks out an EXISTING branch into a worktree
7810
+ - baseBranch: Optional starting point for new branches (defaults to HEAD, commonly "main")
7811
+
7812
+ ---
7813
+
7814
+ ## OUTPUT GUIDELINES
7815
+
7816
+ - Be concise and helpful
7817
+ - Use tools to gather information before responding
7818
+ - When creating sessions, confirm the workspace and project first
7819
+ - For ambiguous requests, ask clarifying questions
7820
+ - Format responses for terminal display (avoid markdown links)
7821
+ - NEVER use tables - they display poorly on mobile devices
7822
+ - ALWAYS use bullet lists or numbered lists instead of tables
7823
+ - Keep list items short and scannable for mobile reading
7824
+ - ALWAYS prioritize safety - check before deleting/merging`;
7612
7825
  return [new HumanMessage(`[System Instructions]
7613
7826
  ${systemPrompt}
7614
7827
  [End Instructions]`)];
@@ -8516,6 +8729,8 @@ var SummarizationService = class {
8516
8729
  buildSystemPrompt() {
8517
8730
  return `You are a concise summarizer for voice output. Summarize AI assistant responses into 1-${this.maxSentences} SHORT sentences.
8518
8731
 
8732
+ CRITICAL: ALWAYS OUTPUT IN ENGLISH. Translate any non-English content to English.
8733
+
8519
8734
  STRICT RULES:
8520
8735
  - Maximum 20 words total (hard limit)
8521
8736
  - Prefer 1 sentence when possible, 2 only if essential
@@ -8523,6 +8738,7 @@ STRICT RULES:
8523
8738
  - Natural spoken language only
8524
8739
  - Never use markdown, bullets, or formatting
8525
8740
  - Never start with "I" - use passive voice or action verbs
8741
+ - ALWAYS translate to English regardless of input language
8526
8742
 
8527
8743
  ABSOLUTELY FORBIDDEN (never include these in output):
8528
8744
  - Session IDs or any alphanumeric identifiers (e.g., "session-abc123", "id: 7f3a2b", UUIDs)
@@ -8530,6 +8746,7 @@ ABSOLUTELY FORBIDDEN (never include these in output):
8530
8746
  - Any string that looks like a technical identifier, hash, or token
8531
8747
  - Code snippets, variable names, or technical jargon
8532
8748
  - Long lists or enumerations
8749
+ - Non-English output (always translate to English)
8533
8750
 
8534
8751
  If the original text contains session IDs or paths, OMIT them entirely. Just describe what happened.
8535
8752
 
@@ -8542,7 +8759,9 @@ GOOD examples:
8542
8759
  BAD examples (never do this):
8543
8760
  - "Created session abc-123-def in /Users/roman/work/project" \u274C
8544
8761
  - "Session ID is 7f3a2b1c" \u274C
8545
- - "Working in /home/user/documents/code" \u274C`;
8762
+ - "Working in /home/user/documents/code" \u274C
8763
+ - "\u0421\u043E\u0437\u0434\u0430\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F Claude." \u274C (non-English)
8764
+ - "Sesi\xF3n creada exitosamente." \u274C (non-English)`;
8546
8765
  }
8547
8766
  /**
8548
8767
  * Builds the user prompt with the text to summarize and optional context.
@@ -9044,22 +9263,29 @@ async function bootstrap() {
9044
9263
  ).map((s) => s.session_id);
9045
9264
  const agentHistoriesMap = chatHistoryService.getAllAgentHistories(agentSessionIds);
9046
9265
  const agentHistories = {};
9047
- for (const [sessionId, history] of agentHistoriesMap) {
9048
- agentHistories[sessionId] = await Promise.all(
9049
- history.map(async (msg) => ({
9050
- sequence: msg.sequence,
9051
- role: msg.role,
9052
- content: msg.content,
9053
- content_blocks: await chatHistoryService.enrichBlocksWithAudio(
9054
- msg.contentBlocks,
9055
- msg.audioOutputPath,
9056
- msg.audioInputPath,
9057
- false
9058
- // Don't include audio in sync.state
9059
- ),
9060
- createdAt: msg.createdAt.toISOString()
9061
- }))
9062
- );
9266
+ const historyEntries = Array.from(agentHistoriesMap.entries());
9267
+ const processedHistories = await Promise.all(
9268
+ historyEntries.map(async ([sessionId, history]) => {
9269
+ const enrichedHistory = await Promise.all(
9270
+ history.map(async (msg) => ({
9271
+ sequence: msg.sequence,
9272
+ role: msg.role,
9273
+ content: msg.content,
9274
+ content_blocks: await chatHistoryService.enrichBlocksWithAudio(
9275
+ msg.contentBlocks,
9276
+ msg.audioOutputPath,
9277
+ msg.audioInputPath,
9278
+ false
9279
+ // Don't include audio in sync.state
9280
+ ),
9281
+ createdAt: msg.createdAt.toISOString()
9282
+ }))
9283
+ );
9284
+ return { sessionId, history: enrichedHistory };
9285
+ })
9286
+ );
9287
+ for (const { sessionId, history } of processedHistories) {
9288
+ agentHistories[sessionId] = history;
9063
9289
  }
9064
9290
  const availableAgentsMap = getAvailableAgents();
9065
9291
  const availableAgents = Array.from(availableAgentsMap.values()).map(
@@ -9193,6 +9419,21 @@ async function bootstrap() {
9193
9419
  }
9194
9420
  let commandText;
9195
9421
  const messageId = commandMessage.payload.message_id;
9422
+ if (messageBroadcaster) {
9423
+ const ackMessageId = messageId || commandMessage.id;
9424
+ const ackMessage = {
9425
+ type: "message.ack",
9426
+ payload: {
9427
+ message_id: ackMessageId,
9428
+ status: "received"
9429
+ }
9430
+ };
9431
+ messageBroadcaster.sendToClient(deviceId, JSON.stringify(ackMessage));
9432
+ logger.debug(
9433
+ { messageId: ackMessageId, deviceId },
9434
+ "Sent message.ack for supervisor.command"
9435
+ );
9436
+ }
9196
9437
  supervisorAgent.resetCancellationState();
9197
9438
  let abortController;
9198
9439
  if (commandMessage.payload.audio) {
@@ -9665,6 +9906,22 @@ async function bootstrap() {
9665
9906
  const deviceId = execMessage.device_id;
9666
9907
  const sessionId = execMessage.session_id;
9667
9908
  const messageId = execMessage.payload.message_id;
9909
+ if (deviceId && messageBroadcaster) {
9910
+ const ackMessageId = messageId || execMessage.id;
9911
+ const ackMessage = {
9912
+ type: "message.ack",
9913
+ payload: {
9914
+ message_id: ackMessageId,
9915
+ session_id: sessionId,
9916
+ status: "received"
9917
+ }
9918
+ };
9919
+ messageBroadcaster.sendToClient(deviceId, JSON.stringify(ackMessage));
9920
+ logger.debug(
9921
+ { messageId: ackMessageId, sessionId, deviceId },
9922
+ "Sent message.ack for session.execute"
9923
+ );
9924
+ }
9668
9925
  cancelledDuringTranscription.delete(sessionId);
9669
9926
  if (execMessage.payload.audio) {
9670
9927
  logger.info(
@@ -10427,7 +10684,7 @@ async function bootstrap() {
10427
10684
  if (persistableBlocks.length > 0) {
10428
10685
  const accumulated = agentMessageAccumulator.get(sessionId) ?? [];
10429
10686
  const wasEmpty = accumulated.length === 0;
10430
- accumulated.push(...persistableBlocks);
10687
+ accumulateBlocks(accumulated, persistableBlocks);
10431
10688
  agentMessageAccumulator.set(sessionId, accumulated);
10432
10689
  if (wasEmpty) {
10433
10690
  logger.debug(
@@ -10482,7 +10739,8 @@ async function bootstrap() {
10482
10739
  }
10483
10740
  }
10484
10741
  const accumulatedBlocks = agentMessageAccumulator.get(sessionId) ?? [];
10485
- const fullAccumulatedText = accumulatedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10742
+ const mergedBlocks = mergeToolBlocks(accumulatedBlocks);
10743
+ const fullAccumulatedText = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10486
10744
  const outputEvent = {
10487
10745
  type: "session.output",
10488
10746
  session_id: sessionId,
@@ -10490,8 +10748,8 @@ async function bootstrap() {
10490
10748
  content_type: "agent",
10491
10749
  content: fullAccumulatedText,
10492
10750
  // Full accumulated text for backward compat
10493
- content_blocks: accumulatedBlocks,
10494
- // Full accumulated blocks for rich UI
10751
+ content_blocks: mergedBlocks,
10752
+ // Full accumulated blocks for rich UI (merged)
10495
10753
  timestamp: Date.now(),
10496
10754
  is_complete: isComplete
10497
10755
  }
@@ -10575,10 +10833,11 @@ async function bootstrap() {
10575
10833
  }
10576
10834
  }
10577
10835
  );
10836
+ let supervisorBlockAccumulator = [];
10578
10837
  supervisorAgent.on(
10579
10838
  "blocks",
10580
10839
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
10581
- async (deviceId, blocks, isComplete, finalOutput, allBlocks) => {
10840
+ async (deviceId, blocks, isComplete, finalOutput, _allBlocks) => {
10582
10841
  const wasCancelled = supervisorAgent.wasCancelled();
10583
10842
  logger.debug(
10584
10843
  { deviceId, blockCount: blocks.length, isComplete, wasCancelled },
@@ -10589,26 +10848,35 @@ async function bootstrap() {
10589
10848
  { deviceId, blockCount: blocks.length, isComplete },
10590
10849
  "Ignoring supervisor blocks - execution was cancelled"
10591
10850
  );
10851
+ supervisorBlockAccumulator = [];
10592
10852
  return;
10593
10853
  }
10594
- const textContent = blocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10854
+ const persistableBlocks = blocks.filter((b) => b.block_type !== "status");
10855
+ if (persistableBlocks.length > 0) {
10856
+ accumulateBlocks(supervisorBlockAccumulator, persistableBlocks);
10857
+ }
10858
+ const mergedBlocks = mergeToolBlocks(supervisorBlockAccumulator);
10859
+ const textContent = mergedBlocks.filter((b) => b.block_type === "text").map((b) => b.content).join("\n");
10595
10860
  const outputEvent = {
10596
10861
  type: "supervisor.output",
10597
10862
  payload: {
10598
10863
  content_type: "supervisor",
10599
10864
  content: textContent,
10600
- content_blocks: blocks,
10865
+ content_blocks: mergedBlocks,
10601
10866
  timestamp: Date.now(),
10602
10867
  is_complete: isComplete
10603
10868
  }
10604
10869
  };
10605
10870
  const message = JSON.stringify(outputEvent);
10606
10871
  broadcaster.broadcastToAll(message);
10872
+ if (isComplete) {
10873
+ supervisorBlockAccumulator = [];
10874
+ }
10607
10875
  if (isComplete && finalOutput && finalOutput.length > 0) {
10608
10876
  chatHistoryService.saveSupervisorMessage(
10609
10877
  "assistant",
10610
10878
  finalOutput,
10611
- allBlocks
10879
+ mergedBlocks
10612
10880
  );
10613
10881
  const pendingVoiceCommand = pendingSupervisorVoiceCommands.get(deviceId);
10614
10882
  if (pendingVoiceCommand && ttsService) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiflis-io/tiflis-code-workstation",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Workstation server for tiflis-code - manages agent sessions and terminal access",
5
5
  "author": "Roman Barinov <rbarinov@gmail.com>",
6
6
  "license": "FSL-1.1-NC",