Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .ai-team/agents/rusty/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,11 @@ The SquadUI extension emerged from initial scaffolding through a rapid sequence
### Team Update: 2026-02-23 - Standup Report Enhancements & Fork-Aware Issues
**Team update (2026-02-23):** Two decisions merged: (1) Standup report issue linkification (#N in AI summaries become clickable GitHub links) with escape-then-linkify pipeline to prevent injection; (2) Velocity chart legend repositioning below canvas for better viewport + accessibility. Fork-aware issue fetching auto-detects upstream repos via GitHub API, with manual override via team.md. decided by @copilot + Rusty

### Charter Editor Command (2026-02-23)
- **Issue:** #72 — Markdown Charter Editor
- **What:** Added `squadui.editCharter` command that opens agent charters in VS Code's text editor with markdown preview side-by-side, enabling in-place editing.
- **Changes:** `package.json` (command + context menu + palette), `src/extension.ts` (command handler), new test file `editCharterCommand.test.ts` (3 tests).
- **Pattern:** Command accepts both string member name and tree item object (for context menu). Uses same slug generation as `viewCharter`. Opens with `preview: false` so edits persist, then fires `markdown.showPreviewToSide` for live preview.
- **Key difference from viewCharter:** `viewCharter` opens read-only markdown preview; `editCharter` opens the text editor (editable) + preview side-by-side.
- **File watcher coverage:** Existing `.ai-team/` file watchers auto-refresh the tree view when charter files are saved — no additional watcher needed.

15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@
"category": "Squad",
"icon": "$(open-preview)"
},
{
"command": "squadui.editCharter",
"title": "Edit Charter",
"category": "Squad",
"icon": "$(edit)"
},
{
"command": "squadui.removeMember",
"title": "Remove Team Member",
Expand Down Expand Up @@ -172,6 +178,11 @@
}
],
"view/item/context": [
{
"command": "squadui.editCharter",
"when": "view == squadTeam && viewItem == member",
"group": "inline"
},
{
"command": "squadui.removeMember",
"when": "view == squadTeam && viewItem == member",
Expand All @@ -194,6 +205,10 @@
}
],
"commandPalette": [
{
"command": "squadui.editCharter",
"when": "false"
},
{
"command": "squadui.showWorkDetails",
"when": "false"
Expand Down
26 changes: 26 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@
if (typeof rawName === 'string') {
memberName = rawName;
} else if (typeof rawName === 'object' && rawName !== null) {
memberName = String((rawName as any).label || (rawName as any).name || '');

Check warning on line 204 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 204 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 204 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (windows-latest)

Unexpected any. Specify a different type

Check warning on line 204 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (windows-latest)

Unexpected any. Specify a different type

Check warning on line 204 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (macos-latest)

Unexpected any. Specify a different type

Check warning on line 204 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (macos-latest)

Unexpected any. Specify a different type
}
const teamRoot = typeof rawRoot === 'string' ? rawRoot : undefined;
if (!memberName) {
Expand All @@ -221,6 +221,32 @@
})
);

// Register edit charter command — opens charter in text editor + markdown preview side-by-side
context.subscriptions.push(
vscode.commands.registerCommand('squadui.editCharter', async (rawName?: unknown) => {
let memberName: string = '';
if (typeof rawName === 'string') {
memberName = rawName;
} else if (typeof rawName === 'object' && rawName !== null) {
memberName = String((rawName as any).label || (rawName as any).memberId || (rawName as any).name || '');

Check warning on line 231 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 231 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 231 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 231 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (windows-latest)

Unexpected any. Specify a different type

Check warning on line 231 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (windows-latest)

Unexpected any. Specify a different type

Check warning on line 231 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (windows-latest)

Unexpected any. Specify a different type

Check warning on line 231 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (macos-latest)

Unexpected any. Specify a different type

Check warning on line 231 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (macos-latest)

Unexpected any. Specify a different type

Check warning on line 231 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (macos-latest)

Unexpected any. Specify a different type
}
if (!memberName) {
vscode.window.showWarningMessage('No member selected');
return;
}
const slug = memberName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
const charterPath = path.join(currentRoot, squadFolderName, 'agents', slug, 'charter.md');
if (!fs.existsSync(charterPath)) {
vscode.window.showWarningMessage(`Charter not found for ${memberName}`);
return;
}
const uri = vscode.Uri.file(charterPath);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
await vscode.commands.executeCommand('markdown.showPreviewToSide', uri);
})
);

// Register squad init command
let allocationPollInterval: ReturnType<typeof setInterval> | undefined;
let initInProgress = false;
Expand Down
220 changes: 220 additions & 0 deletions src/test/suite/editCharterCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* Tests for the editCharter command (squadui.editCharter).
*
* The command opens a charter file in VS Code's text editor with
* markdown preview side-by-side for in-place editing.
*/

import * as assert from 'assert';
import * as path from 'path';
import * as fs from 'fs';
import * as vscode from 'vscode';

suite('EditCharterCommand', () => {
// ─── Command Registration ──────────────────────────────────────────────

suite('Command Registration', () => {
test('editCharter command is registered', async function () {
const extension = vscode.extensions.getExtension('csharpfritz.squadui');
if (!extension || !extension.isActive || !vscode.workspace.workspaceFolders?.length) {
this.skip();
return;
}

const commands = await vscode.commands.getCommands(true);
assert.ok(
commands.includes('squadui.editCharter'),
'squadui.editCharter command should be registered'
);
});

test('editCharter command is declared in package.json', async () => {
const extension = vscode.extensions.getExtension('csharpfritz.squadui');
if (extension) {
const packageJson = extension.packageJSON;
const cmds = packageJson?.contributes?.commands || [];
const hasCmd = cmds.some(
(c: { command: string }) => c.command === 'squadui.editCharter'
);
assert.ok(hasCmd, 'editCharter should be declared in package.json commands');
}
});

test('editCharter has context menu entry for member items', async () => {
const extension = vscode.extensions.getExtension('csharpfritz.squadui');
if (extension) {
const packageJson = extension.packageJSON;
const menus = packageJson?.contributes?.menus?.['view/item/context'] || [];
const hasMenu = menus.some(
(m: { command: string; when: string }) =>
m.command === 'squadui.editCharter' &&
m.when.includes('viewItem == member')
);
assert.ok(hasMenu, 'editCharter should have a context menu entry for member items');
}
});
});

// ─── Opening Charter for Editing ───────────────────────────────────────

suite('Opening Charter for Editing', () => {
test('opens charter.md in text editor for a valid member', async function () {
const commands = await vscode.commands.getCommands(true);
if (!commands.includes('squadui.editCharter')) {
this.skip();
return;
}

const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) {
this.skip();
return;
}

const agentDir = path.join(workspaceRoot, '.ai-team', 'agents', 'edittest');
const charterPath = path.join(agentDir, 'charter.md');
const charterExistedBefore = fs.existsSync(charterPath);

try {
if (!charterExistedBefore) {
fs.mkdirSync(agentDir, { recursive: true });
fs.writeFileSync(charterPath, '# EditTest — Tester\n\nTest charter content.\n', 'utf-8');
}

await vscode.commands.executeCommand('squadui.editCharter', 'EditTest');
await new Promise(resolve => setTimeout(resolve, 500));

const activeEditor = vscode.window.activeTextEditor;
if (activeEditor) {
const activeFilePath = activeEditor.document.uri.fsPath;
assert.ok(
activeFilePath.endsWith(path.join('agents', 'edittest', 'charter.md')),
`Active editor should show charter.md, got: ${activeFilePath}`
);
}
} finally {
if (!charterExistedBefore) {
try {
fs.rmSync(agentDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
}
}
});

test('accepts tree item object with label property', async function () {
const commands = await vscode.commands.getCommands(true);
if (!commands.includes('squadui.editCharter')) {
this.skip();
return;
}

const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) {
this.skip();
return;
}

const agentDir = path.join(workspaceRoot, '.ai-team', 'agents', 'edittest');
const charterPath = path.join(agentDir, 'charter.md');
const charterExistedBefore = fs.existsSync(charterPath);

try {
if (!charterExistedBefore) {
fs.mkdirSync(agentDir, { recursive: true });
fs.writeFileSync(charterPath, '# EditTest — Tester\n\nTest charter content.\n', 'utf-8');
}

// Simulate tree item object (context menu passes tree item)
await vscode.commands.executeCommand('squadui.editCharter', { label: 'EditTest', memberId: 'EditTest' });
await new Promise(resolve => setTimeout(resolve, 500));

const activeEditor = vscode.window.activeTextEditor;
if (activeEditor) {
const activeFilePath = activeEditor.document.uri.fsPath;
assert.ok(
activeFilePath.endsWith(path.join('agents', 'edittest', 'charter.md')),
`Should open charter from tree item object, got: ${activeFilePath}`
);
}
} finally {
if (!charterExistedBefore) {
try {
fs.rmSync(agentDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
}
}
});
});

// ─── Warnings ──────────────────────────────────────────────────────────

suite('Warnings', () => {
test('shows warning when charter not found', async function () {
const commands = await vscode.commands.getCommands(true);
if (!commands.includes('squadui.editCharter')) {
this.skip();
return;
}

if (!vscode.workspace.workspaceFolders?.length) {
this.skip();
return;
}

const origWarn = vscode.window.showWarningMessage;
let warningShown = false;
let warningMessage = '';

try {
(vscode.window as any).showWarningMessage = async (msg: string, ..._items: any[]) => {
warningShown = true;
warningMessage = msg;
return undefined;
};

await vscode.commands.executeCommand('squadui.editCharter', 'NonExistentMember99999');

assert.ok(warningShown, 'Should show a warning when charter file does not exist');
assert.ok(
warningMessage.toLowerCase().includes('charter') ||
warningMessage.toLowerCase().includes('not found'),
`Warning should mention charter or not found, got: "${warningMessage}"`
);
} finally {
(vscode.window as any).showWarningMessage = origWarn;
}
});

test('shows warning when no member selected', async function () {
const commands = await vscode.commands.getCommands(true);
if (!commands.includes('squadui.editCharter')) {
this.skip();
return;
}

if (!vscode.workspace.workspaceFolders?.length) {
this.skip();
return;
}

const origWarn = vscode.window.showWarningMessage;
let warningShown = false;

try {
(vscode.window as any).showWarningMessage = async (_message: string, ..._items: any[]) => {
warningShown = true;
return undefined;
};

await vscode.commands.executeCommand('squadui.editCharter', '');
assert.ok(warningShown, 'Should show a warning when no member is selected');
} finally {
(vscode.window as any).showWarningMessage = origWarn;
}
});
});
});
Loading