tmux-team 3.0.0-alpha.1 → 3.0.0-alpha.2
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 +21 -2
- package/package.json +3 -2
- package/skills/README.md +101 -0
- package/skills/claude/team.md +46 -0
- package/skills/codex/SKILL.md +46 -0
- package/src/cli.ts +19 -0
- package/src/commands/help.ts +1 -0
- package/src/commands/install-skill.ts +148 -0
- package/src/commands/talk.test.ts +192 -47
- package/src/commands/talk.ts +95 -12
package/README.md
CHANGED
|
@@ -80,6 +80,24 @@ eval "$(tmux-team completion bash)"
|
|
|
80
80
|
/plugin install tmux-team@tmux-team
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
### Agent Skills (Optional)
|
|
84
|
+
|
|
85
|
+
Install tmux-team as a native skill for your AI coding agent:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Install for Claude Code (user-wide)
|
|
89
|
+
tmux-team install-skill claude
|
|
90
|
+
|
|
91
|
+
# Install for OpenAI Codex (user-wide)
|
|
92
|
+
tmux-team install-skill codex
|
|
93
|
+
|
|
94
|
+
# Install to project directory instead
|
|
95
|
+
tmux-team install-skill claude --local
|
|
96
|
+
tmux-team install-skill codex --local
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See [skills/README.md](./skills/README.md) for detailed instructions.
|
|
100
|
+
|
|
83
101
|
---
|
|
84
102
|
|
|
85
103
|
## ⌨️ Quick Start
|
|
@@ -133,6 +151,7 @@ Once the plugin is installed, coordinate directly from your Claude Code session:
|
|
|
133
151
|
| `init` | Create `tmux-team.json` in current directory |
|
|
134
152
|
| `config [show/set/clear]` | View/modify settings |
|
|
135
153
|
| `preamble [show/set/clear]` | Manage agent preambles |
|
|
154
|
+
| `install-skill <agent>` | Install skill for Claude/Codex (--local/--user) |
|
|
136
155
|
| `completion [zsh\|bash]` | Output shell completion script |
|
|
137
156
|
|
|
138
157
|
---
|
|
@@ -221,7 +240,7 @@ Use the CLI to manage preambles:
|
|
|
221
240
|
|
|
222
241
|
```bash
|
|
223
242
|
tmux-team preamble show gemini # View current preamble
|
|
224
|
-
tmux-team preamble set gemini "
|
|
243
|
+
tmux-team preamble set gemini "Be concise" # Set preamble
|
|
225
244
|
tmux-team preamble clear gemini # Remove preamble
|
|
226
245
|
```
|
|
227
246
|
|
|
@@ -264,7 +283,7 @@ tmux-team config clear <key> # Clear a config value
|
|
|
264
283
|
|
|
265
284
|
```bash
|
|
266
285
|
tmux-team preamble show <agent> # Show agent's preamble
|
|
267
|
-
tmux-team preamble set <agent> "
|
|
286
|
+
tmux-team preamble set <agent> "text" # Set agent's preamble
|
|
268
287
|
tmux-team preamble clear <agent> # Clear agent's preamble
|
|
269
288
|
```
|
|
270
289
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-team",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.2",
|
|
4
4
|
"description": "CLI tool for AI agent collaboration in tmux - manage cross-pane communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
],
|
|
43
43
|
"files": [
|
|
44
44
|
"bin",
|
|
45
|
-
"src"
|
|
45
|
+
"src",
|
|
46
|
+
"skills"
|
|
46
47
|
],
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"tsx": "^4.7.0"
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Agent Skills Installation
|
|
2
|
+
|
|
3
|
+
tmux-team provides pre-built skills for popular AI coding agents.
|
|
4
|
+
|
|
5
|
+
## Quick Install
|
|
6
|
+
|
|
7
|
+
Use the CLI to install skills automatically:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install for Claude Code (user-wide)
|
|
11
|
+
tmux-team install-skill claude
|
|
12
|
+
|
|
13
|
+
# Install for OpenAI Codex (user-wide)
|
|
14
|
+
tmux-team install-skill codex
|
|
15
|
+
|
|
16
|
+
# Install to project directory (local scope)
|
|
17
|
+
tmux-team install-skill claude --local
|
|
18
|
+
tmux-team install-skill codex --local
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Claude Code
|
|
22
|
+
|
|
23
|
+
Claude Code uses slash commands stored in `~/.claude/commands/` (user) or `.claude/commands/` (local).
|
|
24
|
+
|
|
25
|
+
### Manual Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
mkdir -p ~/.claude/commands
|
|
29
|
+
cp skills/claude/team.md ~/.claude/commands/team.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# In Claude Code, use the slash command:
|
|
36
|
+
/team talk codex "Review this PR"
|
|
37
|
+
|
|
38
|
+
# Or invoke implicitly - Claude will recognize when to use it
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## OpenAI Codex CLI
|
|
42
|
+
|
|
43
|
+
Codex uses skills stored in `~/.codex/skills/<skill-name>/` (user) or `.codex/skills/<skill-name>/` (local).
|
|
44
|
+
|
|
45
|
+
### Manual Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
mkdir -p ~/.codex/skills/tmux-team
|
|
49
|
+
cp skills/codex/SKILL.md ~/.codex/skills/tmux-team/SKILL.md
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Enable Skills (Required)
|
|
53
|
+
|
|
54
|
+
Skills require the feature flag:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
codex --enable skills
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or set in your config to enable by default.
|
|
61
|
+
|
|
62
|
+
### Usage
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Explicit invocation
|
|
66
|
+
$tmux-team talk codex "Review this PR"
|
|
67
|
+
|
|
68
|
+
# Implicit - Codex auto-selects when you mention other agents
|
|
69
|
+
"Ask the codex agent to review the authentication code"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Gemini CLI
|
|
73
|
+
|
|
74
|
+
Gemini CLI doesn't have a native skill system yet. Use the preamble feature instead:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"gemini": {
|
|
79
|
+
"pane": "%2",
|
|
80
|
+
"preamble": "You can communicate with other agents using tmux-team CLI. Run `tmux-team help` to learn more."
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Save this to `tmux-team.json` in your project root.
|
|
86
|
+
|
|
87
|
+
## Verify Installation
|
|
88
|
+
|
|
89
|
+
After installation, verify the skill is recognized:
|
|
90
|
+
|
|
91
|
+
**Claude Code:**
|
|
92
|
+
```
|
|
93
|
+
/help
|
|
94
|
+
# Should show: /team - Talk to peer agents...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Codex:**
|
|
98
|
+
```
|
|
99
|
+
/skills
|
|
100
|
+
# Should show: tmux-team
|
|
101
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
allowed-tools: Bash(tmux-team:*)
|
|
3
|
+
description: Talk to peer agents in different tmux panes
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Execute this tmux-team command: `tmux-team $ARGUMENTS`
|
|
7
|
+
|
|
8
|
+
You are working in a multi-agent tmux environment.
|
|
9
|
+
Use the tmux-team CLI to communicate with other agents.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Send message to an agent
|
|
15
|
+
tmux-team talk codex "your message"
|
|
16
|
+
tmux-team talk gemini "your message"
|
|
17
|
+
tmux-team talk all "broadcast message"
|
|
18
|
+
|
|
19
|
+
# Send with delay (useful for rate limiting)
|
|
20
|
+
tmux-team talk codex "message" --delay 5
|
|
21
|
+
|
|
22
|
+
# Send and wait for response (blocks until agent replies)
|
|
23
|
+
tmux-team talk codex "message" --wait --timeout 120
|
|
24
|
+
|
|
25
|
+
# Read agent response (default: 100 lines)
|
|
26
|
+
tmux-team check codex
|
|
27
|
+
tmux-team check gemini 200
|
|
28
|
+
|
|
29
|
+
# List all configured agents
|
|
30
|
+
tmux-team list
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
1. Send message: `tmux-team talk codex "Review this code"`
|
|
36
|
+
2. Wait 5-15 seconds (or use `--wait` flag)
|
|
37
|
+
3. Read response: `tmux-team check codex`
|
|
38
|
+
4. If response is cut off: `tmux-team check codex 200`
|
|
39
|
+
|
|
40
|
+
## Notes
|
|
41
|
+
|
|
42
|
+
- `talk` automatically sends Enter key after the message
|
|
43
|
+
- `talk` automatically filters exclamation marks for Gemini (TTY issue)
|
|
44
|
+
- Use `--delay` instead of sleep (safer for tool whitelists)
|
|
45
|
+
- Use `--wait` for synchronous request-response patterns
|
|
46
|
+
- Run `tmux-team help` for full CLI documentation
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tmux-team
|
|
3
|
+
description: Communicate with other AI agents in tmux panes. Use when you need to talk to codex, claude, gemini, or other agents.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
When invoked, execute the tmux-team command with the provided arguments.
|
|
7
|
+
|
|
8
|
+
You are working in a multi-agent tmux environment.
|
|
9
|
+
Use the tmux-team CLI to communicate with other agents.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Send message to an agent
|
|
15
|
+
tmux-team talk codex "your message"
|
|
16
|
+
tmux-team talk gemini "your message"
|
|
17
|
+
tmux-team talk all "broadcast message"
|
|
18
|
+
|
|
19
|
+
# Send with delay (useful for rate limiting)
|
|
20
|
+
tmux-team talk codex "message" --delay 5
|
|
21
|
+
|
|
22
|
+
# Send and wait for response (blocks until agent replies)
|
|
23
|
+
tmux-team talk codex "message" --wait --timeout 120
|
|
24
|
+
|
|
25
|
+
# Read agent response (default: 100 lines)
|
|
26
|
+
tmux-team check codex
|
|
27
|
+
tmux-team check gemini 200
|
|
28
|
+
|
|
29
|
+
# List all configured agents
|
|
30
|
+
tmux-team list
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
1. Send message: `tmux-team talk codex "Review this code"`
|
|
36
|
+
2. Wait 5-15 seconds (or use `--wait` flag)
|
|
37
|
+
3. Read response: `tmux-team check codex`
|
|
38
|
+
4. If response is cut off: `tmux-team check codex 200`
|
|
39
|
+
|
|
40
|
+
## Notes
|
|
41
|
+
|
|
42
|
+
- `talk` automatically sends Enter key after the message
|
|
43
|
+
- `talk` automatically filters exclamation marks for Gemini (TTY issue)
|
|
44
|
+
- Use `--delay` instead of sleep (safer for tool whitelists)
|
|
45
|
+
- Use `--wait` for synchronous request-response patterns
|
|
46
|
+
- Run `tmux-team help` for full CLI documentation
|
package/src/cli.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { cmdCheck } from './commands/check.js';
|
|
|
19
19
|
import { cmdCompletion } from './commands/completion.js';
|
|
20
20
|
import { cmdConfig } from './commands/config.js';
|
|
21
21
|
import { cmdPreamble } from './commands/preamble.js';
|
|
22
|
+
import { cmdInstallSkill } from './commands/install-skill.js';
|
|
22
23
|
|
|
23
24
|
// ─────────────────────────────────────────────────────────────
|
|
24
25
|
// Argument parsing
|
|
@@ -210,6 +211,24 @@ function main(): void {
|
|
|
210
211
|
cmdPreamble(ctx, args);
|
|
211
212
|
break;
|
|
212
213
|
|
|
214
|
+
case 'install-skill':
|
|
215
|
+
{
|
|
216
|
+
// Parse --local or --user flag
|
|
217
|
+
let scope = 'user';
|
|
218
|
+
const filteredArgs: string[] = [];
|
|
219
|
+
for (const arg of args) {
|
|
220
|
+
if (arg === '--local') {
|
|
221
|
+
scope = 'local';
|
|
222
|
+
} else if (arg === '--user') {
|
|
223
|
+
scope = 'user';
|
|
224
|
+
} else {
|
|
225
|
+
filteredArgs.push(arg);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
cmdInstallSkill(ctx, filteredArgs[0], scope);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
|
|
213
232
|
default:
|
|
214
233
|
ctx.ui.error(`Unknown command: ${command}. Run 'tmux-team help' for usage.`);
|
|
215
234
|
ctx.exit(ExitCodes.ERROR);
|
package/src/commands/help.ts
CHANGED
|
@@ -42,6 +42,7 @@ ${colors.yellow('COMMANDS')}
|
|
|
42
42
|
${colors.green('init')} Create empty tmux-team.json
|
|
43
43
|
${colors.green('config')} [show|set|clear] View/modify settings
|
|
44
44
|
${colors.green('preamble')} [show|set|clear] Manage agent preambles
|
|
45
|
+
${colors.green('install-skill')} <agent> Install skill for AI agent
|
|
45
46
|
${colors.green('completion')} Output shell completion script
|
|
46
47
|
${colors.green('help')} Show this help message
|
|
47
48
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// install-skill command - install tmux-team skills for AI agents
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as os from 'node:os';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import type { Context } from '../types.js';
|
|
10
|
+
import { ExitCodes } from '../context.js';
|
|
11
|
+
|
|
12
|
+
type AgentType = 'claude' | 'codex';
|
|
13
|
+
type Scope = 'user' | 'local';
|
|
14
|
+
|
|
15
|
+
interface SkillConfig {
|
|
16
|
+
sourceFile: string;
|
|
17
|
+
userDir: string;
|
|
18
|
+
localDir: string;
|
|
19
|
+
targetFile: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getCodexHome(): string {
|
|
23
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SKILL_CONFIGS: Record<AgentType, SkillConfig> = {
|
|
27
|
+
claude: {
|
|
28
|
+
sourceFile: 'skills/claude/team.md',
|
|
29
|
+
userDir: path.join(os.homedir(), '.claude', 'commands'),
|
|
30
|
+
localDir: '.claude/commands',
|
|
31
|
+
targetFile: 'team.md',
|
|
32
|
+
},
|
|
33
|
+
codex: {
|
|
34
|
+
sourceFile: 'skills/codex/SKILL.md',
|
|
35
|
+
userDir: path.join(getCodexHome(), 'skills', 'tmux-team'),
|
|
36
|
+
localDir: '.codex/skills/tmux-team',
|
|
37
|
+
targetFile: 'SKILL.md',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const SUPPORTED_AGENTS = Object.keys(SKILL_CONFIGS) as AgentType[];
|
|
42
|
+
|
|
43
|
+
function findPackageRoot(): string {
|
|
44
|
+
// Get current file's directory (ES modules don't have __dirname)
|
|
45
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
46
|
+
let dir = path.dirname(currentFile);
|
|
47
|
+
|
|
48
|
+
// Try to find the package root by looking for package.json
|
|
49
|
+
for (let i = 0; i < 5; i++) {
|
|
50
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
51
|
+
if (fs.existsSync(pkgPath)) {
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
const parent = path.dirname(dir);
|
|
55
|
+
if (parent === dir) break;
|
|
56
|
+
dir = parent;
|
|
57
|
+
}
|
|
58
|
+
// Fallback: assume we're in src/commands
|
|
59
|
+
return path.resolve(path.dirname(currentFile), '..', '..');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function exitWithError(ctx: Context, error: string, hint?: string): never {
|
|
63
|
+
if (ctx.flags.json) {
|
|
64
|
+
ctx.ui.json({ success: false, error, hint });
|
|
65
|
+
} else {
|
|
66
|
+
ctx.ui.error(error);
|
|
67
|
+
if (hint) ctx.ui.info(hint);
|
|
68
|
+
}
|
|
69
|
+
ctx.exit(ExitCodes.ERROR);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function cmdInstallSkill(ctx: Context, agent?: string, scope: string = 'user'): void {
|
|
73
|
+
// Validate agent
|
|
74
|
+
if (!agent) {
|
|
75
|
+
exitWithError(
|
|
76
|
+
ctx,
|
|
77
|
+
'Usage: tmux-team install-skill <agent> [--local|--user]',
|
|
78
|
+
`Supported agents: ${SUPPORTED_AGENTS.join(', ')}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const agentLower = agent.toLowerCase() as AgentType;
|
|
83
|
+
if (!SUPPORTED_AGENTS.includes(agentLower)) {
|
|
84
|
+
exitWithError(
|
|
85
|
+
ctx,
|
|
86
|
+
`Unknown agent: ${agent}`,
|
|
87
|
+
`Supported agents: ${SUPPORTED_AGENTS.join(', ')}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Validate scope
|
|
92
|
+
const scopeLower = scope.toLowerCase() as Scope;
|
|
93
|
+
if (scopeLower !== 'user' && scopeLower !== 'local') {
|
|
94
|
+
exitWithError(ctx, `Invalid scope: ${scope}. Use 'user' or 'local'.`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const config = SKILL_CONFIGS[agentLower];
|
|
98
|
+
const pkgRoot = findPackageRoot();
|
|
99
|
+
const sourcePath = path.join(pkgRoot, config.sourceFile);
|
|
100
|
+
|
|
101
|
+
// Check source file exists
|
|
102
|
+
if (!fs.existsSync(sourcePath)) {
|
|
103
|
+
exitWithError(
|
|
104
|
+
ctx,
|
|
105
|
+
`Skill file not found: ${sourcePath}`,
|
|
106
|
+
'Make sure tmux-team is properly installed.'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Determine target directory
|
|
111
|
+
const targetDir = scopeLower === 'user' ? config.userDir : path.resolve(config.localDir);
|
|
112
|
+
const targetPath = path.join(targetDir, config.targetFile);
|
|
113
|
+
|
|
114
|
+
// Check if already exists
|
|
115
|
+
if (fs.existsSync(targetPath) && !ctx.flags.force) {
|
|
116
|
+
exitWithError(ctx, `Skill already exists: ${targetPath}`, 'Use --force to overwrite.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Create directory if needed
|
|
120
|
+
if (!fs.existsSync(targetDir)) {
|
|
121
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
122
|
+
if (ctx.flags.verbose) {
|
|
123
|
+
ctx.ui.info(`Created directory: ${targetDir}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Copy file
|
|
128
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
129
|
+
|
|
130
|
+
if (ctx.flags.json) {
|
|
131
|
+
ctx.ui.json({
|
|
132
|
+
success: true,
|
|
133
|
+
agent: agentLower,
|
|
134
|
+
scope: scopeLower,
|
|
135
|
+
path: targetPath,
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
ctx.ui.success(`Installed ${agentLower} skill to ${targetPath}`);
|
|
139
|
+
|
|
140
|
+
// Show usage hint
|
|
141
|
+
if (agentLower === 'claude') {
|
|
142
|
+
ctx.ui.info('Usage: /team talk codex "message"');
|
|
143
|
+
} else if (agentLower === 'codex') {
|
|
144
|
+
ctx.ui.info('Enable skills: codex --enable skills');
|
|
145
|
+
ctx.ui.info('Usage: $tmux-team or implicit invocation');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -443,6 +443,12 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
443
443
|
}
|
|
444
444
|
});
|
|
445
445
|
|
|
446
|
+
// Helper: generate mock capture output with proper marker structure
|
|
447
|
+
// The end marker must appear TWICE: once in instruction, once from "agent"
|
|
448
|
+
function mockCompleteResponse(nonce: string, response: string): string {
|
|
449
|
+
return `{tmux-team-start:${nonce}}\nHello\n\n[IMPORTANT: When your response is complete, print exactly: {tmux-team-end:${nonce}}]\n${response}\n{tmux-team-end:${nonce}}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
446
452
|
it('appends nonce instruction to message', async () => {
|
|
447
453
|
const tmux = createMockTmux();
|
|
448
454
|
// Set up capture to return the nonce marker immediately
|
|
@@ -450,10 +456,10 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
450
456
|
tmux.capture = () => {
|
|
451
457
|
captureCount++;
|
|
452
458
|
if (captureCount === 1) return ''; // Baseline
|
|
453
|
-
// Return marker on second capture
|
|
459
|
+
// Return marker on second capture - must include instruction AND agent's end marker
|
|
454
460
|
const sent = tmux.sends[0]?.message || '';
|
|
455
461
|
const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
456
|
-
return match ?
|
|
462
|
+
return match ? mockCompleteResponse(match[1], 'Response here') : '';
|
|
457
463
|
};
|
|
458
464
|
|
|
459
465
|
const ctx = createContext({
|
|
@@ -492,7 +498,7 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
492
498
|
const sent = tmux.sends[0]?.message || '';
|
|
493
499
|
const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
494
500
|
if (match) {
|
|
495
|
-
return
|
|
501
|
+
return mockCompleteResponse(match[1], 'Agent response here');
|
|
496
502
|
}
|
|
497
503
|
return 'baseline content';
|
|
498
504
|
};
|
|
@@ -563,14 +569,15 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
563
569
|
const oldContent = 'Previous conversation\nOld content here';
|
|
564
570
|
|
|
565
571
|
tmux.capture = () => {
|
|
566
|
-
// Simulate scrollback with old content,
|
|
572
|
+
// Simulate scrollback with old content, then our instruction (with end marker), response, and agent's end marker
|
|
567
573
|
const sent = tmux.sends[0]?.message || '';
|
|
568
574
|
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
569
575
|
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
570
576
|
if (startMatch && endMatch) {
|
|
571
577
|
// Start and end markers should have the same nonce
|
|
572
578
|
expect(startMatch[1]).toBe(endMatch[1]);
|
|
573
|
-
|
|
579
|
+
// Must include end marker TWICE: once in instruction, once from "agent"
|
|
580
|
+
return `${oldContent}\n\n{tmux-team-start:${startMatch[1]}}\nMessage content here\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nNew response content\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
574
581
|
}
|
|
575
582
|
return oldContent;
|
|
576
583
|
};
|
|
@@ -610,7 +617,7 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
610
617
|
if (captureCount === 1) return '';
|
|
611
618
|
const sent = tmux.sends[0]?.message || '';
|
|
612
619
|
const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
613
|
-
return match ?
|
|
620
|
+
return match ? mockCompleteResponse(match[1], 'Done') : '';
|
|
614
621
|
};
|
|
615
622
|
|
|
616
623
|
const paths = createTestPaths(testDir);
|
|
@@ -673,22 +680,22 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
673
680
|
// Create mock tmux that returns markers for each agent after a delay
|
|
674
681
|
const tmux = createMockTmux();
|
|
675
682
|
let captureCount = 0;
|
|
676
|
-
const
|
|
683
|
+
const noncesByPane: Record<string, string> = {};
|
|
677
684
|
|
|
678
|
-
// Mock send to capture the
|
|
685
|
+
// Mock send to capture the nonce for each pane
|
|
679
686
|
tmux.send = (pane: string, msg: string) => {
|
|
680
687
|
const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
681
688
|
if (match) {
|
|
682
|
-
|
|
689
|
+
noncesByPane[pane] = match[1];
|
|
683
690
|
}
|
|
684
691
|
};
|
|
685
692
|
|
|
686
|
-
// Mock capture to return
|
|
693
|
+
// Mock capture to return complete response after first poll
|
|
687
694
|
tmux.capture = (pane: string) => {
|
|
688
695
|
captureCount++;
|
|
689
|
-
// Return
|
|
690
|
-
if (captureCount > 3 &&
|
|
691
|
-
return
|
|
696
|
+
// Return complete response on second capture for each pane
|
|
697
|
+
if (captureCount > 3 && noncesByPane[pane]) {
|
|
698
|
+
return mockCompleteResponse(noncesByPane[pane], 'Response from agent');
|
|
692
699
|
}
|
|
693
700
|
return 'working...';
|
|
694
701
|
};
|
|
@@ -722,19 +729,19 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
722
729
|
|
|
723
730
|
it('handles partial timeout in wait mode with all target', async () => {
|
|
724
731
|
const tmux = createMockTmux();
|
|
725
|
-
const
|
|
732
|
+
const noncesByPane: Record<string, string> = {};
|
|
726
733
|
|
|
727
734
|
tmux.send = (pane: string, msg: string) => {
|
|
728
735
|
const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
729
736
|
if (match) {
|
|
730
|
-
|
|
737
|
+
noncesByPane[pane] = match[1];
|
|
731
738
|
}
|
|
732
739
|
};
|
|
733
740
|
|
|
734
741
|
// Only pane 10.1 responds, 10.2 times out
|
|
735
742
|
tmux.capture = (pane: string) => {
|
|
736
|
-
if (pane === '10.1' &&
|
|
737
|
-
return
|
|
743
|
+
if (pane === '10.1' && noncesByPane[pane]) {
|
|
744
|
+
return mockCompleteResponse(noncesByPane[pane], 'Response from codex');
|
|
738
745
|
}
|
|
739
746
|
return 'still working...';
|
|
740
747
|
};
|
|
@@ -789,11 +796,11 @@ describe('cmdTalk - --wait mode', () => {
|
|
|
789
796
|
}
|
|
790
797
|
};
|
|
791
798
|
|
|
792
|
-
// Return
|
|
799
|
+
// Return complete response immediately
|
|
793
800
|
tmux.capture = (pane: string) => {
|
|
794
801
|
const idx = pane === '10.1' ? 0 : 1;
|
|
795
802
|
if (nonces[idx]) {
|
|
796
|
-
return
|
|
803
|
+
return mockCompleteResponse(nonces[idx], 'Response');
|
|
797
804
|
}
|
|
798
805
|
return '';
|
|
799
806
|
};
|
|
@@ -861,12 +868,13 @@ describe('cmdTalk - nonce collision handling', () => {
|
|
|
861
868
|
}
|
|
862
869
|
return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
|
|
863
870
|
}
|
|
864
|
-
// Finally, new end marker appears
|
|
871
|
+
// Finally, new end marker appears - must have TWO occurrences of new end marker
|
|
865
872
|
const sent = tmux.sends[0]?.message || '';
|
|
866
873
|
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
867
874
|
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
868
875
|
if (startMatch && endMatch) {
|
|
869
|
-
|
|
876
|
+
// Old markers in scrollback + new instruction (with end marker) + response + agent's end marker
|
|
877
|
+
return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}\n\n{tmux-team-start:${startMatch[1]}}\nNew question asked\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nNew response\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
870
878
|
}
|
|
871
879
|
return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
|
|
872
880
|
};
|
|
@@ -916,10 +924,10 @@ describe('cmdTalk - JSON output contract', () => {
|
|
|
916
924
|
|
|
917
925
|
tmux.capture = () => {
|
|
918
926
|
const sent = tmux.sends[0]?.message || '';
|
|
919
|
-
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
920
927
|
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
921
|
-
if (
|
|
922
|
-
|
|
928
|
+
if (endMatch) {
|
|
929
|
+
// Must have TWO end markers: one in instruction, one from "agent"
|
|
930
|
+
return `{tmux-team-start:${endMatch[1]}}\nMessage\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nResponse\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
923
931
|
}
|
|
924
932
|
return '';
|
|
925
933
|
};
|
|
@@ -988,6 +996,140 @@ describe('cmdTalk - JSON output contract', () => {
|
|
|
988
996
|
expect(output).toHaveProperty('startMarker');
|
|
989
997
|
expect(output).toHaveProperty('endMarker');
|
|
990
998
|
});
|
|
999
|
+
|
|
1000
|
+
it('captures partialResponse on timeout when agent started responding', async () => {
|
|
1001
|
+
const tmux = createMockTmux();
|
|
1002
|
+
const ui = createMockUI();
|
|
1003
|
+
|
|
1004
|
+
// Simulate agent started responding but didn't finish (no end marker)
|
|
1005
|
+
tmux.capture = () =>
|
|
1006
|
+
'{tmux-team-start:abcd}\nHello\n\n[IMPORTANT: When your response is complete, print exactly: {tmux-team-end:abcd}]\nThis is partial content\nStill writing...';
|
|
1007
|
+
|
|
1008
|
+
const ctx = createContext({
|
|
1009
|
+
tmux,
|
|
1010
|
+
ui,
|
|
1011
|
+
paths: createTestPaths(testDir),
|
|
1012
|
+
flags: { wait: true, json: true, timeout: 0.05 },
|
|
1013
|
+
config: {
|
|
1014
|
+
defaults: {
|
|
1015
|
+
timeout: 0.05,
|
|
1016
|
+
pollInterval: 0.01,
|
|
1017
|
+
captureLines: 100,
|
|
1018
|
+
preambleEvery: 3,
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
try {
|
|
1024
|
+
await cmdTalk(ctx, 'claude', 'Hello');
|
|
1025
|
+
} catch {
|
|
1026
|
+
// Expected timeout
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
1030
|
+
expect(output).toHaveProperty('status', 'timeout');
|
|
1031
|
+
expect(output).toHaveProperty('partialResponse');
|
|
1032
|
+
expect(output.partialResponse).toContain('This is partial content');
|
|
1033
|
+
expect(output.partialResponse).toContain('Still writing...');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('returns null partialResponse when nothing captured', async () => {
|
|
1037
|
+
const tmux = createMockTmux();
|
|
1038
|
+
const ui = createMockUI();
|
|
1039
|
+
|
|
1040
|
+
// Nothing meaningful in the capture
|
|
1041
|
+
tmux.capture = () => 'random scrollback content';
|
|
1042
|
+
|
|
1043
|
+
const ctx = createContext({
|
|
1044
|
+
tmux,
|
|
1045
|
+
ui,
|
|
1046
|
+
paths: createTestPaths(testDir),
|
|
1047
|
+
flags: { wait: true, json: true, timeout: 0.05 },
|
|
1048
|
+
config: {
|
|
1049
|
+
defaults: {
|
|
1050
|
+
timeout: 0.05,
|
|
1051
|
+
pollInterval: 0.01,
|
|
1052
|
+
captureLines: 100,
|
|
1053
|
+
preambleEvery: 3,
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
await cmdTalk(ctx, 'claude', 'Hello');
|
|
1060
|
+
} catch {
|
|
1061
|
+
// Expected timeout
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const output = ui.jsonOutput[0] as Record<string, unknown>;
|
|
1065
|
+
expect(output).toHaveProperty('status', 'timeout');
|
|
1066
|
+
expect(output.partialResponse).toBeNull();
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it('captures partialResponse in broadcast timeout', async () => {
|
|
1070
|
+
const tmux = createMockTmux();
|
|
1071
|
+
const ui = createMockUI();
|
|
1072
|
+
const markersByPane: Record<string, string> = {};
|
|
1073
|
+
|
|
1074
|
+
tmux.send = (pane: string, msg: string) => {
|
|
1075
|
+
const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1076
|
+
if (match) markersByPane[pane] = match[1];
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// codex completes, gemini times out with partial response
|
|
1080
|
+
tmux.capture = (pane: string) => {
|
|
1081
|
+
if (pane === '10.1') {
|
|
1082
|
+
const nonce = markersByPane['10.1'];
|
|
1083
|
+
return `{tmux-team-start:${nonce}}\nMsg\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\nResponse\n{tmux-team-end:${nonce}}`;
|
|
1084
|
+
}
|
|
1085
|
+
// gemini has partial response
|
|
1086
|
+
const nonce = markersByPane['10.2'];
|
|
1087
|
+
return `{tmux-team-start:${nonce}}\nMsg\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\nPartial gemini output...`;
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const paths = createTestPaths(testDir);
|
|
1091
|
+
const ctx = createContext({
|
|
1092
|
+
ui,
|
|
1093
|
+
tmux,
|
|
1094
|
+
paths,
|
|
1095
|
+
flags: { wait: true, timeout: 0.1, json: true },
|
|
1096
|
+
config: {
|
|
1097
|
+
defaults: {
|
|
1098
|
+
timeout: 0.1,
|
|
1099
|
+
pollInterval: 0.02,
|
|
1100
|
+
captureLines: 100,
|
|
1101
|
+
preambleEvery: 3,
|
|
1102
|
+
},
|
|
1103
|
+
paneRegistry: {
|
|
1104
|
+
codex: { pane: '10.1' },
|
|
1105
|
+
gemini: { pane: '10.2' },
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
try {
|
|
1111
|
+
await cmdTalk(ctx, 'all', 'Hello');
|
|
1112
|
+
} catch {
|
|
1113
|
+
// Expected timeout exit
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const result = ui.jsonOutput[0] as {
|
|
1117
|
+
results: Array<{
|
|
1118
|
+
agent: string;
|
|
1119
|
+
status: string;
|
|
1120
|
+
response?: string;
|
|
1121
|
+
partialResponse?: string;
|
|
1122
|
+
}>;
|
|
1123
|
+
};
|
|
1124
|
+
const codexResult = result.results.find((r) => r.agent === 'codex');
|
|
1125
|
+
const geminiResult = result.results.find((r) => r.agent === 'gemini');
|
|
1126
|
+
|
|
1127
|
+
expect(codexResult?.status).toBe('completed');
|
|
1128
|
+
expect(codexResult?.response).toContain('Response');
|
|
1129
|
+
|
|
1130
|
+
expect(geminiResult?.status).toBe('timeout');
|
|
1131
|
+
expect(geminiResult?.partialResponse).toContain('Partial gemini output');
|
|
1132
|
+
});
|
|
991
1133
|
});
|
|
992
1134
|
|
|
993
1135
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -1007,17 +1149,22 @@ describe('cmdTalk - start/end marker extraction', () => {
|
|
|
1007
1149
|
}
|
|
1008
1150
|
});
|
|
1009
1151
|
|
|
1152
|
+
// Helper: generate mock capture output with proper marker structure
|
|
1153
|
+
// The end marker must appear TWICE: once in instruction, once from "agent"
|
|
1154
|
+
function mockResponse(nonce: string, response: string): string {
|
|
1155
|
+
return `{tmux-team-start:${nonce}}\nContent\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\n${response}\n{tmux-team-end:${nonce}}`;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1010
1158
|
it('includes both start and end markers in sent message', async () => {
|
|
1011
1159
|
const tmux = createMockTmux();
|
|
1012
1160
|
const ui = createMockUI();
|
|
1013
1161
|
|
|
1014
|
-
// Return
|
|
1162
|
+
// Return complete response immediately
|
|
1015
1163
|
tmux.capture = () => {
|
|
1016
1164
|
const sent = tmux.sends[0]?.message || '';
|
|
1017
|
-
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1018
1165
|
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1019
|
-
if (
|
|
1020
|
-
return
|
|
1166
|
+
if (endMatch) {
|
|
1167
|
+
return mockResponse(endMatch[1], 'Response');
|
|
1021
1168
|
}
|
|
1022
1169
|
return '';
|
|
1023
1170
|
};
|
|
@@ -1048,11 +1195,10 @@ describe('cmdTalk - start/end marker extraction', () => {
|
|
|
1048
1195
|
|
|
1049
1196
|
tmux.capture = () => {
|
|
1050
1197
|
const sent = tmux.sends[0]?.message || '';
|
|
1051
|
-
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1052
1198
|
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1053
|
-
if (
|
|
1054
|
-
// Simulate scrollback with content before start marker,
|
|
1055
|
-
return `Old garbage\nMore old stuff\n{tmux-team-start:${
|
|
1199
|
+
if (endMatch) {
|
|
1200
|
+
// Simulate scrollback with content before start marker, then proper instruction + response
|
|
1201
|
+
return `Old garbage\nMore old stuff\n{tmux-team-start:${endMatch[1]}}\nThe original message\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nThis is the actual response\n\n{tmux-team-end:${endMatch[1]}}\nContent after marker`;
|
|
1056
1202
|
}
|
|
1057
1203
|
return 'Old garbage\nMore old stuff';
|
|
1058
1204
|
};
|
|
@@ -1085,10 +1231,9 @@ Line 4 final`;
|
|
|
1085
1231
|
|
|
1086
1232
|
tmux.capture = () => {
|
|
1087
1233
|
const sent = tmux.sends[0]?.message || '';
|
|
1088
|
-
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1089
1234
|
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1090
|
-
if (
|
|
1091
|
-
return `{tmux-team-start:${
|
|
1235
|
+
if (endMatch) {
|
|
1236
|
+
return `{tmux-team-start:${endMatch[1]}}\nMessage\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\n${multilineResponse}\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
1092
1237
|
}
|
|
1093
1238
|
return '';
|
|
1094
1239
|
};
|
|
@@ -1114,11 +1259,10 @@ Line 4 final`;
|
|
|
1114
1259
|
|
|
1115
1260
|
tmux.capture = () => {
|
|
1116
1261
|
const sent = tmux.sends[0]?.message || '';
|
|
1117
|
-
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1118
1262
|
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1119
|
-
if (
|
|
1120
|
-
//
|
|
1121
|
-
return `{tmux-team-start:${
|
|
1263
|
+
if (endMatch) {
|
|
1264
|
+
// Agent printed end marker immediately with no content
|
|
1265
|
+
return `{tmux-team-start:${endMatch[1]}}\nMessage here\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\n{tmux-team-end:${endMatch[1]}}`;
|
|
1122
1266
|
}
|
|
1123
1267
|
return '';
|
|
1124
1268
|
};
|
|
@@ -1145,11 +1289,10 @@ Line 4 final`;
|
|
|
1145
1289
|
|
|
1146
1290
|
tmux.capture = () => {
|
|
1147
1291
|
const sent = tmux.sends[0]?.message || '';
|
|
1148
|
-
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1149
1292
|
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1150
|
-
if (
|
|
1151
|
-
// Start marker followed by newline, then content
|
|
1152
|
-
return `Old stuff\n{tmux-team-start:${
|
|
1293
|
+
if (endMatch) {
|
|
1294
|
+
// Start marker followed by newline, then instruction and content
|
|
1295
|
+
return `Old stuff\n{tmux-team-start:${endMatch[1]}}\nMessage\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nActual response content\n{tmux-team-end:${endMatch[1]}}`;
|
|
1153
1296
|
}
|
|
1154
1297
|
return 'Old stuff';
|
|
1155
1298
|
};
|
|
@@ -1182,10 +1325,11 @@ Line 4 final`;
|
|
|
1182
1325
|
if (startMatch && endMatch) {
|
|
1183
1326
|
if (captureCount < 3) {
|
|
1184
1327
|
// First few captures: old markers in history, new start marker sent but not end yet
|
|
1185
|
-
|
|
1328
|
+
// (only ONE end marker in instruction = still waiting)
|
|
1329
|
+
return `{tmux-team-start:old1}\nOld message\nOld response\n{tmux-team-end:old1}\n\n{tmux-team-start:${startMatch[1]}}\nNew message\n[When done: {tmux-team-end:${endMatch[1]}}]`;
|
|
1186
1330
|
}
|
|
1187
|
-
// Finally, new end marker appears
|
|
1188
|
-
return `{tmux-team-start:old1}\nOld message\nOld response\n{tmux-team-end:old1}\n\n{tmux-team-start:${startMatch[1]}}\nNew message\n\nNew actual response\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
1331
|
+
// Finally, new end marker appears (TWO occurrences = complete)
|
|
1332
|
+
return `{tmux-team-start:old1}\nOld message\nOld response\n{tmux-team-end:old1}\n\n{tmux-team-start:${startMatch[1]}}\nNew message\n[When done: {tmux-team-end:${endMatch[1]}}]\n\nNew actual response\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
1189
1333
|
}
|
|
1190
1334
|
return '';
|
|
1191
1335
|
};
|
|
@@ -1219,7 +1363,8 @@ Line 4 final`;
|
|
|
1219
1363
|
const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
|
|
1220
1364
|
const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
|
|
1221
1365
|
if (startMatch && endMatch) {
|
|
1222
|
-
|
|
1366
|
+
// TWO end markers: one in instruction, one from "agent" response
|
|
1367
|
+
return `${lotsOfContent}\n{tmux-team-start:${startMatch[1]}}\nMessage\n[When done: {tmux-team-end:${endMatch[1]}}]\n\nThe actual response\n\n{tmux-team-end:${endMatch[1]}}`;
|
|
1223
1368
|
}
|
|
1224
1369
|
return lotsOfContent;
|
|
1225
1370
|
};
|
package/src/commands/talk.ts
CHANGED
|
@@ -32,6 +32,41 @@ function renderWaitLine(agent: string, elapsedSeconds: number): string {
|
|
|
32
32
|
return `⏳ Waiting for ${agent}... (${s}s)`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Extract partial response from output when end marker is not found.
|
|
37
|
+
* Used to capture whatever the agent wrote before timeout.
|
|
38
|
+
*/
|
|
39
|
+
function extractPartialResponse(
|
|
40
|
+
output: string,
|
|
41
|
+
startMarker: string,
|
|
42
|
+
_endMarker: string
|
|
43
|
+
): string | null {
|
|
44
|
+
// Look for instruction end pattern `}]` (the instruction ends with `{end}]`)
|
|
45
|
+
const instructionEndPattern = '}]';
|
|
46
|
+
const instructionEndIndex = output.lastIndexOf(instructionEndPattern);
|
|
47
|
+
|
|
48
|
+
let responseStart = 0;
|
|
49
|
+
if (instructionEndIndex !== -1) {
|
|
50
|
+
// Find the first newline after the instruction's closing `}]`
|
|
51
|
+
responseStart = output.indexOf('\n', instructionEndIndex + 2);
|
|
52
|
+
if (responseStart !== -1) responseStart += 1;
|
|
53
|
+
else responseStart = instructionEndIndex + 2;
|
|
54
|
+
} else {
|
|
55
|
+
// Fallback: try to find newline after start marker
|
|
56
|
+
const startMarkerIndex = output.lastIndexOf(startMarker);
|
|
57
|
+
if (startMarkerIndex !== -1) {
|
|
58
|
+
responseStart = output.indexOf('\n', startMarkerIndex);
|
|
59
|
+
if (responseStart !== -1) responseStart += 1;
|
|
60
|
+
else return null; // Can't find response start
|
|
61
|
+
} else {
|
|
62
|
+
return null; // Start marker not found
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const partial = output.slice(responseStart).trim();
|
|
67
|
+
return partial || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
35
70
|
// ─────────────────────────────────────────────────────────────
|
|
36
71
|
// Types for broadcast wait mode
|
|
37
72
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -45,6 +80,7 @@ interface AgentWaitState {
|
|
|
45
80
|
endMarker: string;
|
|
46
81
|
status: 'pending' | 'completed' | 'timeout' | 'error';
|
|
47
82
|
response?: string;
|
|
83
|
+
partialResponse?: string | null;
|
|
48
84
|
error?: string;
|
|
49
85
|
elapsedMs?: number;
|
|
50
86
|
}
|
|
@@ -251,8 +287,22 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
|
|
|
251
287
|
const elapsedSeconds = (Date.now() - startedAt) / 1000;
|
|
252
288
|
if (elapsedSeconds >= timeoutSeconds) {
|
|
253
289
|
clearActiveRequest(ctx.paths, target, requestId);
|
|
290
|
+
|
|
291
|
+
// Capture partial response on timeout
|
|
292
|
+
let partialResponse: string | null = null;
|
|
293
|
+
try {
|
|
294
|
+
const output = tmux.capture(pane, captureLines);
|
|
295
|
+
const extracted = extractPartialResponse(output, startMarker, endMarker);
|
|
296
|
+
if (extracted) partialResponse = extracted;
|
|
297
|
+
} catch {
|
|
298
|
+
// Ignore capture errors on timeout
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isTTY) {
|
|
302
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
303
|
+
}
|
|
304
|
+
|
|
254
305
|
if (flags.json) {
|
|
255
|
-
// Single JSON output with error field (don't call ui.error separately)
|
|
256
306
|
ui.json({
|
|
257
307
|
target,
|
|
258
308
|
pane,
|
|
@@ -262,13 +312,17 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
|
|
|
262
312
|
nonce,
|
|
263
313
|
startMarker,
|
|
264
314
|
endMarker,
|
|
315
|
+
partialResponse,
|
|
265
316
|
});
|
|
266
317
|
exit(ExitCodes.TIMEOUT);
|
|
267
318
|
}
|
|
268
|
-
|
|
269
|
-
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
270
|
-
}
|
|
319
|
+
|
|
271
320
|
ui.error(`Timed out waiting for ${target} after ${Math.floor(timeoutSeconds)}s.`);
|
|
321
|
+
if (partialResponse) {
|
|
322
|
+
console.log();
|
|
323
|
+
console.log(colors.yellow(`─── Partial response from ${target} (${pane}) ───`));
|
|
324
|
+
console.log(partialResponse);
|
|
325
|
+
}
|
|
272
326
|
exit(ExitCodes.TIMEOUT);
|
|
273
327
|
}
|
|
274
328
|
|
|
@@ -277,7 +331,7 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
|
|
|
277
331
|
process.stdout.write('\r' + renderWaitLine(target, elapsedSeconds));
|
|
278
332
|
} else {
|
|
279
333
|
const now = Date.now();
|
|
280
|
-
if (now - lastNonTtyLogAt >=
|
|
334
|
+
if (now - lastNonTtyLogAt >= 30000) {
|
|
281
335
|
lastNonTtyLogAt = now;
|
|
282
336
|
console.error(
|
|
283
337
|
`[tmux-team] Waiting for ${target} (${Math.floor(elapsedSeconds)}s elapsed)`
|
|
@@ -297,9 +351,16 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
|
|
|
297
351
|
exit(ExitCodes.ERROR);
|
|
298
352
|
}
|
|
299
353
|
|
|
300
|
-
// Find end marker
|
|
301
|
-
|
|
302
|
-
|
|
354
|
+
// Find end marker - it appears once in our instruction and again when agent prints it
|
|
355
|
+
// We need TWO occurrences: one in instruction + one from agent = complete
|
|
356
|
+
// Only ONE occurrence means it's just in instruction = still waiting
|
|
357
|
+
const firstEndMarkerIndex = output.indexOf(endMarker);
|
|
358
|
+
const lastEndMarkerIndex = output.lastIndexOf(endMarker);
|
|
359
|
+
if (firstEndMarkerIndex === -1 || firstEndMarkerIndex === lastEndMarkerIndex) {
|
|
360
|
+
// No marker or only one (in instruction) - still waiting
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const endMarkerIndex = lastEndMarkerIndex;
|
|
303
364
|
|
|
304
365
|
// Find the end of our instruction by looking for `}]` pattern (the instruction ends with `{end}]`)
|
|
305
366
|
// This is more reliable than looking for newline after start marker because
|
|
@@ -467,6 +528,16 @@ async function cmdTalkAllWait(
|
|
|
467
528
|
state.status = 'timeout';
|
|
468
529
|
state.error = `Timed out after ${Math.floor(timeoutSeconds)}s`;
|
|
469
530
|
state.elapsedMs = Math.floor(elapsedSeconds * 1000);
|
|
531
|
+
|
|
532
|
+
// Capture partial response on timeout
|
|
533
|
+
try {
|
|
534
|
+
const output = tmux.capture(state.pane, captureLines);
|
|
535
|
+
const extracted = extractPartialResponse(output, state.startMarker, state.endMarker);
|
|
536
|
+
if (extracted) state.partialResponse = extracted;
|
|
537
|
+
} catch {
|
|
538
|
+
// Ignore capture errors on timeout
|
|
539
|
+
}
|
|
540
|
+
|
|
470
541
|
clearActiveRequest(paths, state.agent, state.requestId);
|
|
471
542
|
if (!flags.json) {
|
|
472
543
|
console.log(
|
|
@@ -482,7 +553,7 @@ async function cmdTalkAllWait(
|
|
|
482
553
|
// Progress logging (non-TTY)
|
|
483
554
|
if (!flags.json && !isTTY) {
|
|
484
555
|
const now = Date.now();
|
|
485
|
-
if (now - lastLogAt >=
|
|
556
|
+
if (now - lastLogAt >= 30000) {
|
|
486
557
|
lastLogAt = now;
|
|
487
558
|
const pending = pendingAgents()
|
|
488
559
|
.map((s) => s.agent)
|
|
@@ -511,9 +582,16 @@ async function cmdTalkAllWait(
|
|
|
511
582
|
continue;
|
|
512
583
|
}
|
|
513
584
|
|
|
514
|
-
// Find end marker
|
|
515
|
-
|
|
516
|
-
|
|
585
|
+
// Find end marker - it appears once in our instruction and again when agent prints it
|
|
586
|
+
// We need TWO occurrences: one in instruction + one from agent = complete
|
|
587
|
+
// Only ONE occurrence means it's just in instruction = still waiting
|
|
588
|
+
const firstEndMarkerIndex = output.indexOf(state.endMarker);
|
|
589
|
+
const lastEndMarkerIndex = output.lastIndexOf(state.endMarker);
|
|
590
|
+
if (firstEndMarkerIndex === -1 || firstEndMarkerIndex === lastEndMarkerIndex) {
|
|
591
|
+
// No marker or only one (in instruction) - still waiting
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const endMarkerIndex = lastEndMarkerIndex;
|
|
517
595
|
|
|
518
596
|
// Find the end of our instruction by looking for `}]` pattern (the instruction ends with `{end}]`)
|
|
519
597
|
// This is more reliable than looking for newline after start marker because
|
|
@@ -602,6 +680,7 @@ function outputBroadcastResults(
|
|
|
602
680
|
endMarker: s.endMarker,
|
|
603
681
|
status: s.status,
|
|
604
682
|
response: s.response,
|
|
683
|
+
partialResponse: s.partialResponse,
|
|
605
684
|
error: s.error,
|
|
606
685
|
elapsedMs: s.elapsedMs,
|
|
607
686
|
})),
|
|
@@ -623,6 +702,10 @@ function outputBroadcastResults(
|
|
|
623
702
|
console.log(colors.cyan(`─── Response from ${state.agent} (${state.pane}) ───`));
|
|
624
703
|
console.log(state.response);
|
|
625
704
|
console.log();
|
|
705
|
+
} else if (state.status === 'timeout' && state.partialResponse) {
|
|
706
|
+
console.log(colors.yellow(`─── Partial response from ${state.agent} (${state.pane}) ───`));
|
|
707
|
+
console.log(state.partialResponse);
|
|
708
|
+
console.log();
|
|
626
709
|
}
|
|
627
710
|
}
|
|
628
711
|
}
|