Skip to content

Commit 50f9513

Browse files
committed
release: v0.1.0-alpha.6
- Collapsible AI thinking steps with Ctrl+O toggle - Professional terminal UI (no emojis, clean badges) - Rich tool-call formatting matching agy native style - Inline code badges and underlined link rendering - Sentence-boundary line breaking for thinking output - agy banner suppression (filter internal headers) - Interactive context preserved across stream completion - Plan file path extraction fix - Ctrl+O shortcut shown in CLI startup banner - Update release.yml to auto-extract changelog per tag
1 parent 3eefb31 commit 50f9513

43 files changed

Lines changed: 1617 additions & 280 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/release.yml‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ jobs:
3939
- name: Package Standalone CLI Executables
4040
run: pnpm --filter @collagility/cli package
4141

42+
- name: Extract Changelog Entry
43+
id: changelog
44+
run: |
45+
# Extract the changelog block for the current tag
46+
TAG="${{ github.ref_name }}"
47+
VERSION="${TAG#v}"
48+
BODY=$(awk "/## \[v${VERSION}\]/{found=1; next} found && /^---/{exit} found{print}" CHANGELOG.md)
49+
echo "CHANGELOG_BODY<<EOF" >> $GITHUB_OUTPUT
50+
echo "$BODY" >> $GITHUB_OUTPUT
51+
echo "EOF" >> $GITHUB_OUTPUT
52+
4253
- name: Create GitHub Release & Upload Assets
4354
uses: softprops/action-gh-release@v2
4455
with:
@@ -54,17 +65,30 @@ jobs:
5465
body: |
5566
# Collagility ${{ github.ref_name }}
5667
57-
🚀 **Single-Command Installation**
68+
> The Multiplayer Terminal for AI Coding Agents
69+
70+
## Install / Update
5871
5972
```bash
6073
curl -fsSL https://raw.githubusercontent.com/JhaSourav07/Collagility/main/install.sh | bash
6174
```
6275
76+
The installer auto-detects your OS & architecture, downloads the matching binary, and installs it to `~/.local/bin/collagility`.
77+
78+
---
79+
80+
${{ steps.changelog.outputs.CHANGELOG_BODY }}
81+
82+
---
83+
6384
## Release Assets
64-
- **Linux (x64)**: `collagility-linux-x64.tar.gz`
65-
- **Linux (arm64)**: `collagility-linux-arm64.tar.gz`
66-
- **macOS (Intel x64)**: `collagility-macos-x64.tar.gz`
67-
- **macOS (Apple Silicon arm64)**: `collagility-macos-arm64.tar.gz`
68-
- **Windows (x64)**: `collagility-windows-x64.zip`
85+
86+
| Platform | Architecture | File |
87+
|---|---|---|
88+
| Linux | x64 (AMD64) | `collagility-linux-x64.tar.gz` |
89+
| Linux | arm64 (AArch64) | `collagility-linux-arm64.tar.gz` |
90+
| macOS | Intel x64 | `collagility-macos-x64.tar.gz` |
91+
| macOS | Apple Silicon (M1/M2/M3) | `collagility-macos-arm64.tar.gz` |
92+
| Windows | x64 | `collagility-windows-x64.zip` |
6993
env:
7094
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

‎CHANGELOG.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,34 @@ All notable changes to Collagility will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [v0.1.0-alpha.6] - 2026-08-01
9+
10+
### Added
11+
12+
- **Collapsible AI Thinking Steps**: AI thinking output (internal analysis steps) is now collapsed by default into a single compact summary line (`● Thinking... (N steps)`). Press `Ctrl+O` at any time to expand/collapse the full step-by-step reasoning in-place.
13+
- **`Ctrl+O` Shortcut Hint**: The `Ctrl+O` toggle shortcut is now displayed in the CLI startup banner, so users see it immediately on session start.
14+
- **Inline Code Badges**: Backtick-wrapped code references in AI responses now render with a dark slate highlight background for quick visual scanning (e.g. `` `pnpm` `` → styled badge).
15+
- **Clickable Link Rendering**: Markdown hyperlinks in AI responses (e.g. `[Collagility](file://...)`) now render as underlined cyan text.
16+
- **Rich Tool Call Formatting**: AI tool invocations (e.g. `ListDir(...)`, `ViewFile(...)`) now render with a green bullet `●`, bold yellow tool name, and dim argument text — matching the native Antigravity IDE style.
17+
- **Tree Branch Results**: Tool sub-results (lines starting with `⎿` or `└`) render as dim tree branches (`└ result text`).
18+
19+
### Changed
20+
21+
- **Professional Badge Text**: All AI provider badges now use clean all-caps text without emojis — e.g. `[ANTIGRAVITY]`, `[GEMINI]`, `[CLAUDE]`, `[CODEX]`, `[AI]`.
22+
- **No-Emoji Terminal UI**: Removed all emojis from interactive prompt banners (`[AI Question]`, `[TOOL APPROVAL REQUESTED]`, `[AI Confirmation Required]`), plan renderer, and system messages. The terminal is now clean, professional, and accessible in all environments.
23+
- **Sentence-Boundary Line Breaking**: AI thinking output is now automatically split at sentence boundaries (`I'll...`, `Let's...`, `I see...`, `Now...`) so each thought renders on its own line instead of running together.
24+
25+
### Fixed
26+
27+
- **agy Banner Suppression**: `agy`'s own internal header lines (`🤖 AGY Stream Started: "..."`, `Prompt: "..."`) are now filtered out in the parser so they no longer appear as raw text in the terminal. Only Collagility's styled header renders.
28+
- **Interactive Context Preserved Across Stream Completion**: When `agy` finishes streaming and `onStreamCompleted` fires, any active interactive context (plan approval, question response) is now retained until the user explicitly responds — typing `r`, `y`, or `n` no longer accidentally triggers a new AI prompt.
29+
- **Plan File Path Extraction**: Plan artifact file paths (e.g. `[plan.md](file:///path/to/plan.md)`) are now correctly stripped of markdown link syntax before `fs.readFileSync` is called, fixing the `Plan file not found` error.
30+
31+
---
32+
833
## [v0.1.0-alpha.2] - 2026-07-31
934

35+
1036
### Added (done)
1137

1238
- **Multi-Provider AI Adapters (done)**: Added full provider adapters for `claude`, `codex`, `aider`, and `goose` in `@collagility/adapters`.

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> **The Multiplayer Terminal for AI Coding Agents**
44
5-
[![Release](https://img.shields.io/badge/version-v0.1.0--alpha.1-cyan.svg)](https://github.com/JhaSourav07/Collagility/releases)
5+
[![Release](https://img.shields.io/badge/version-v0.1.0--alpha.6-cyan.svg)](https://github.com/JhaSourav07/Collagility/releases)
66
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
77
[![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg)]()
88

@@ -124,6 +124,7 @@ Inside an active session terminal:
124124

125125
- [x] **v0.1.0-alpha.1 (done)**: Multiplayer WebSocket relay, AI stream chunking, `agy`/`gemini` CLI adapter, workspace execution, terminal renderer.
126126
- [x] **v0.1.0-alpha.2 (done)**: Multi-provider adapters (`claude`, `codex`, `aider`, `goose`), permissions guard, binary packager, and release CI workflow.
127+
- [x] **v0.1.0-alpha.6 (done)**: Professional terminal UI overhaul — collapsible AI thinking steps (`Ctrl+O`), inline code badges, rich tool-call formatting, agy banner suppression, no-emoji clean output.
127128
- [ ] **v0.2.0 (future)**: IDE extension integration (VS Code & Antigravity IDE plugin).
128129
- [ ] **v0.3.0 (future)**: Interactive co-driver permission approvals (peer code review for agent tool calls).
129130
- [ ] **v0.4.0 (future)**: Multi-agent consensus swarm routing and Redis cluster scaling.

‎RELEASE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Collagility v0.1.0-alpha.1
1+
# Collagility v0.1.0-alpha.5
22

33
🚀 **First Public Alpha Release**
44

‎apps/cli/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@collagility/cli",
3-
"version": "0.1.0-alpha.1",
3+
"version": "0.1.0-alpha.6",
44
"private": true,
55
"description": "Terminal CLI application for Collagility host and participant sessions",
66
"type": "module",

‎apps/cli/src/client/ws-client.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ export interface WSClientEvents {
4646
onStreamCancelled?: (payload: AIStreamCancelledPayload) => void;
4747
onStreamFailed?: (payload: AIStreamFailedPayload) => void;
4848
onStreamError?: (payload: AIStreamErrorPayload) => void;
49+
onQuestion?: (payload: any) => void;
50+
onPlan?: (payload: any) => void;
51+
onSelection?: (payload: any) => void;
52+
onConfirmation?: (payload: any) => void;
53+
onToolRequest?: (payload: any) => void;
54+
onFrame?: (frame: IncomingFrame) => void;
4955
onError?: (error: string, code?: string) => void;
5056
onDisconnect?: (reason: string) => void;
5157
onReconnecting?: (attempt: number, delayMs: number) => void;
@@ -57,8 +63,13 @@ export class WebSocketClient {
5763
private reconnectHandler: ReconnectHandler;
5864
private events: WSClientEvents = {};
5965
private clientId: string | null = null;
66+
private sessionId: string | null = null;
6067
private isExplicitClose = false;
6168

69+
public getSessionId(): string | null {
70+
return this.sessionId;
71+
}
72+
6273
constructor(config: CLIConfig, events: WSClientEvents = {}) {
6374
this.config = config;
6475
this.events = events;
@@ -127,6 +138,10 @@ export class WebSocketClient {
127138
}
128139

129140
private handleFrame(frame: IncomingFrame, resolveConnect?: (clientId: string) => void): void {
141+
if (this.events.onFrame) {
142+
this.events.onFrame(frame);
143+
}
144+
130145
switch (frame.type) {
131146
case 'system.connected': {
132147
const payload = frame.payload as { clientId: string };
@@ -142,12 +157,14 @@ export class WebSocketClient {
142157

143158
case 'session.created': {
144159
const payload = frame.payload as { session: Record<string, unknown> };
160+
this.sessionId = (payload.session as any)?.id || null;
145161
if (this.events.onSessionCreated) this.events.onSessionCreated(payload.session);
146162
break;
147163
}
148164

149165
case 'session.joined': {
150166
const payload = frame.payload as { session: Record<string, unknown>; memberId: string };
167+
this.sessionId = (payload.session as any)?.id || null;
151168
if (this.events.onSessionJoined) this.events.onSessionJoined(payload.session, payload.memberId);
152169
break;
153170
}
@@ -237,6 +254,31 @@ export class WebSocketClient {
237254
break;
238255
}
239256

257+
case 'ai.question': {
258+
if (this.events.onQuestion) this.events.onQuestion(frame.payload);
259+
break;
260+
}
261+
262+
case 'ai.plan': {
263+
if (this.events.onPlan) this.events.onPlan(frame.payload);
264+
break;
265+
}
266+
267+
case 'ai.selection': {
268+
if (this.events.onSelection) this.events.onSelection(frame.payload);
269+
break;
270+
}
271+
272+
case 'ai.confirmation': {
273+
if (this.events.onConfirmation) this.events.onConfirmation(frame.payload);
274+
break;
275+
}
276+
277+
case 'ai.tool.request': {
278+
if (this.events.onToolRequest) this.events.onToolRequest(frame.payload);
279+
break;
280+
}
281+
240282
case 'system.error':
241283
case 'session.error': {
242284
const payload = frame.payload as { error: string; code?: string };
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { buildServer, type ServerInstance } from '@collagility/server';
3+
import { WebSocketClient } from '../client/ws-client.js';
4+
import { createConfig } from '../config/config.js';
5+
import { StreamManager } from '@collagility/stream';
6+
import { MockAIAdapter } from '@collagility/adapters';
7+
8+
describe('Interactive AI Workflow Integration', () => {
9+
let serverInstance: ServerInstance;
10+
let serverUrl: string;
11+
12+
beforeEach(async () => {
13+
serverInstance = buildServer();
14+
const address = await serverInstance.listen(0, '127.0.0.1');
15+
const port = (serverInstance.app.server.address() as { port: number }).port;
16+
serverUrl = `ws://127.0.0.1:${port}/ws`;
17+
});
18+
19+
afterEach(async () => {
20+
await serverInstance.close();
21+
});
22+
23+
it('should process bidirectional questions, plans, confirmations and tool approvals', async () => {
24+
const ownerClient = new WebSocketClient(createConfig({
25+
serverUrl,
26+
maxReconnectAttempts: 1,
27+
reconnectIntervalMs: 100,
28+
}));
29+
30+
const memberClient = new WebSocketClient(createConfig({
31+
serverUrl,
32+
maxReconnectAttempts: 1,
33+
reconnectIntervalMs: 100,
34+
}));
35+
36+
await ownerClient.connect();
37+
await memberClient.connect();
38+
39+
let createdSessionId = '';
40+
let ownerJoined = false;
41+
let memberReceivedPlan = false;
42+
let memberReceivedQuestion = false;
43+
44+
// Set up owner session creation
45+
ownerClient.createSession();
46+
await new Promise<void>((resolve) => {
47+
ownerClient['events'].onSessionCreated = (session) => {
48+
createdSessionId = String(session['id']);
49+
ownerJoined = true;
50+
resolve();
51+
};
52+
});
53+
54+
expect(ownerJoined).toBe(true);
55+
expect(createdSessionId).toBeTruthy();
56+
57+
// Join member client
58+
memberClient.joinSession(createdSessionId);
59+
await new Promise<void>((resolve) => {
60+
memberClient['events'].onSessionJoined = () => resolve();
61+
});
62+
63+
// Subscribe member to interactive events
64+
memberClient['events'].onPlan = (payload) => {
65+
if (payload.title === 'Refactor Monorepo Structure') {
66+
memberReceivedPlan = true;
67+
}
68+
};
69+
70+
memberClient['events'].onQuestion = (payload) => {
71+
if (payload.prompt.includes('Which framework')) {
72+
memberReceivedQuestion = true;
73+
}
74+
};
75+
76+
// Owner broadcasts interactive AI Plan
77+
ownerClient.send('ai.plan', {
78+
planId: 'plan-101',
79+
streamId: 'stream-1',
80+
title: 'Refactor Monorepo Structure',
81+
steps: ['Create package', 'Migrate code', 'Run tests'],
82+
requiresApproval: true,
83+
});
84+
85+
// Owner broadcasts interactive AI Question
86+
ownerClient.send('ai.question', {
87+
questionId: 'q-101',
88+
streamId: 'stream-1',
89+
prompt: 'Which framework would you like to use?',
90+
options: ['Next.js', 'Vite'],
91+
});
92+
93+
await new Promise((r) => setTimeout(r, 200));
94+
95+
expect(memberReceivedPlan).toBe(true);
96+
expect(memberReceivedQuestion).toBe(true);
97+
98+
// Owner approves plan & answers question
99+
let planApprovedBroadcast = false;
100+
let questionAnswerBroadcast = false;
101+
102+
memberClient['events'].onFrame = (frame) => {
103+
if (frame.type === 'ai.plan.approve') {
104+
planApprovedBroadcast = true;
105+
}
106+
if (frame.type === 'ai.answer') {
107+
questionAnswerBroadcast = true;
108+
}
109+
};
110+
111+
ownerClient.send('ai.plan.approve', { planId: 'plan-101', streamId: 'stream-1' });
112+
ownerClient.send('ai.answer', { questionId: 'q-101', streamId: 'stream-1', answer: 'Next.js' });
113+
114+
await new Promise((r) => setTimeout(r, 200));
115+
116+
expect(planApprovedBroadcast).toBe(true);
117+
expect(questionAnswerBroadcast).toBe(true);
118+
119+
ownerClient.disconnect();
120+
memberClient.disconnect();
121+
});
122+
123+
it('should clean up completed stream state and support consecutive AI prompts', async () => {
124+
const streamManager = new StreamManager();
125+
const sessionId = 'test-session-multi-prompt';
126+
const ownerId = 'owner-123';
127+
128+
// First stream
129+
const stream1 = streamManager.startStream({
130+
sessionId,
131+
ownerId,
132+
prompt: 'First Prompt',
133+
adapterName: 'mock',
134+
});
135+
136+
expect(streamManager.isStreamActive(sessionId)).toBe(true);
137+
138+
// Complete first stream
139+
streamManager.completeStream(sessionId);
140+
141+
// Verify stream is no longer active and cleaned up
142+
expect(streamManager.isStreamActive(sessionId)).toBe(false);
143+
144+
// Second stream must succeed without throwing STREAM_ALREADY_ACTIVE
145+
const stream2 = streamManager.startStream({
146+
sessionId,
147+
ownerId,
148+
prompt: 'Second Prompt',
149+
adapterName: 'mock',
150+
});
151+
152+
expect(stream2.streamId).not.toBe(stream1.streamId);
153+
expect(streamManager.isStreamActive(sessionId)).toBe(true);
154+
155+
streamManager.completeStream(sessionId);
156+
expect(streamManager.isStreamActive(sessionId)).toBe(false);
157+
});
158+
});

0 commit comments

Comments
 (0)