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
7,865 changes: 3,146 additions & 4,719 deletions package-lock.json

Large diffs are not rendered by default.

86 changes: 43 additions & 43 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,76 +80,76 @@
"prepare": "husky"
},
"dependencies": {
"@github/copilot": "^1.0.45",
"@github/copilot-sdk": "^0.3.0",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/office-runtime": "^1.0.35",
"@github/copilot": "^1.0.60",
"@github/copilot-sdk": "^1.0.0",
"@radix-ui/react-collapsible": "^1.1.13",
"@radix-ui/react-dialog": "^1.1.16",
"@radix-ui/react-popover": "^1.1.16",
"@radix-ui/react-slot": "^1.2.5",
"@radix-ui/react-tooltip": "^1.2.9",
"@types/office-runtime": "^1.0.37",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cors": "^2.8.6",
"express": "^5.2.1",
"jszip": "^3.10.1",
"lucide-react": "^0.577.0",
"lucide-react": "^1.17.0",
"pptxgenjs": "^4.0.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"skillpm": "^0.0.12",
"tailwind-merge": "^3.5.0",
"skillpm": "^1.0.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"vscode-jsonrpc": "^8.2.1",
"ws": "^8.20.1",
"zustand": "^5.0.11"
"ws": "^8.21.0",
"zustand": "^5.0.14"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@modelcontextprotocol/sdk": "^1.26.0",
"@playwright/test": "^1.58.2",
"@tailwindcss/postcss": "^4.2.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@playwright/test": "^1.60.0",
"@tailwindcss/postcss": "^4.3.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/mocha": "^10.0.10",
"@types/office-js": "^1.0.574",
"@types/react": "^19.2.14",
"@types/office-js": "^1.0.592",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/eslint-plugin": "^1.6.12",
"@vitejs/plugin-react": "^6.0.2",
"@vitest/coverage-v8": "^4.1.8",
"@vitest/eslint-plugin": "^1.6.19",
"@vscode/codicons": "^0.0.45",
"dotenv": "^17.3.1",
"electron": "^41.1.0",
"electron-builder": "^26.0.12",
"eslint": "^10.0.3",
"dotenv": "^17.4.2",
"electron": "^42.3.3",
"electron-builder": "^26.15.2",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.3.0",
"globals": "^17.6.0",
"husky": "^9.1.7",
"jsdom": "^29.0.0",
"lint-staged": "^16.2.7",
"mocha": "^11.7.5",
"office-addin-debugging": "^6.0.6",
"office-addin-dev-certs": "^2.0.6",
"office-addin-dev-settings": "^3.0.6",
"office-addin-mock": "^3.0.6",
"jsdom": "^29.1.1",
"lint-staged": "^17.0.7",
"mocha": "^11.7.6",
"office-addin-debugging": "^6.1.1",
"office-addin-dev-certs": "^2.0.9",
"office-addin-dev-settings": "^3.1.1",
"office-addin-mock": "^3.0.9",
"patch-package": "^8.0.1",
"postcss": "^8.5.10",
"prettier": "^3.8.1",
"tailwindcss": "^4.1.18",
"postcss": "^8.5.15",
"prettier": "^3.8.4",
"tailwindcss": "^4.3.0",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0",
"vite": "^8.0.5",
"vite-plugin-static-copy": "^3.2.0",
"vitest": "^4.0.18"
"typescript": "^6.0.3",
"typescript-eslint": "^8.61.0",
"vite": "^8.0.16",
"vite-plugin-static-copy": "^4.1.1",
"vitest": "^4.1.8"
},
"engines": {
"node": ">=20.0.0"
Expand Down
6 changes: 2 additions & 4 deletions src/components/chat/ToolProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,7 @@ const ToolProgressImpl: React.FC<ToolProgressProps> = ({ part }) => {
{isCancelled ? 'Cancelled:' : 'Error:'}
</p>
<p style={{ color: 'var(--vscode-errorForeground)' }}>
{typeof status.error === 'string'
? status.error
: JSON.stringify(status.error as object)}
{typeof status.error === 'string' ? status.error : JSON.stringify(status.error)}
</p>
</div>
)}
Expand All @@ -103,7 +101,7 @@ const ToolProgressImpl: React.FC<ToolProgressProps> = ({ part }) => {
<div className="tool-details-section">
<p className="tool-details-label">Output</p>
<pre className="tool-details-code">
{typeof result === 'string' ? result : JSON.stringify(result as object, null, 2)}
{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
</pre>
</div>
)}
Expand Down
37 changes: 36 additions & 1 deletion src/copilotProxy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ import { WebSocketServer } from 'ws';
import { CopilotClient } from '@github/copilot-sdk';
import { randomUUID } from 'node:crypto';
import { isTrustedRequestOrigin } from './serverSecurity.mjs';
import { getInstalledOfficePluginDirectories } from './plugins/cliPluginBootstrap.mjs';

/**
* Installed Office CLI plugin directories, resolved once. Passed to every
* createSession so the SDK loads plugin-owned agents (office-excel:excel, …);
* SDK >=1.0 no longer auto-discovers marketplace-installed plugins.
*/
let _officePluginDirs = null;
function officePluginDirectories() {
if (_officePluginDirs === null) {
_officePluginDirs = getInstalledOfficePluginDirectories();
console.log(
`[proxy] Office plugin directories: ${
_officePluginDirs.length ? _officePluginDirs.join(', ') : '(none found)'
}`
);
}
return _officePluginDirs;
}

const MCP_STATUSES = new Set([
'connected',
Expand Down Expand Up @@ -409,6 +428,21 @@ async function handleConnection(ws) {
let session;
try {
await ensureStarted();
const pluginDirectories = officePluginDirectories();
const requestedAgent =
typeof agent === 'string' && agent.length > 0 ? agent : undefined;
// SDK >=1.0 only loads plugin-owned agents (office-excel:excel, …)
// from explicit pluginDirectories. If an agent is requested but no
// plugin directories were found, createSession fails with a cryptic
// "Custom agent '…' not found" — surface the real cause up front.
if (requestedAgent && pluginDirectories.length === 0) {
console.warn(
`[proxy] Agent '${requestedAgent}' was requested but no Office plugin ` +
`directories were found. Plugin-owned agents will not resolve. ` +
`Ensure the required CLI plugins are installed (startup bootstrap), ` +
`and check COPILOT_HOME if set.`
);
}
session = await client.createSession({
clientName: 'office-coding-agent',
model,
Expand All @@ -417,7 +451,8 @@ async function handleConnection(ws) {
tools,
mcpServers,
availableTools,
agent: typeof agent === 'string' && agent.length > 0 ? agent : undefined,
pluginDirectories,
agent: requestedAgent,
onPermissionRequest: async request => {
console.log(`[proxy] permission.request received: ${request.kind}`);
const decision = await requestPermissionDecision(session.sessionId, request);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useDeckOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export async function orchestrateDeck(
// --- Phase 2: Workers (batched) ---
const results: SlideProgress[] = plan.slides.map(slide => ({
plan: slide,
status: 'pending' as SlideStatus,
status: 'pending',
}));

const pptTools = getToolsForHost('powerpoint');
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useDocumentOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export async function orchestrateDocument(
// --- Phase 2: Workers (batched) ---
const results: SectionProgress[] = plan.sections.map(section => ({
plan: section,
status: 'pending' as SectionStatus,
status: 'pending',
}));

const wordTools = getToolsForHost('word');
Expand Down
12 changes: 5 additions & 7 deletions src/hooks/useOfficeChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,12 +467,10 @@ export function useOfficeChat(host: OfficeHostApp) {
current?.requestId === event.data.requestId ? null : current
);
const selectedMode = event.data.selectedAction;
if (
selectedMode === 'interactive' ||
selectedMode === 'plan' ||
selectedMode === 'autopilot'
) {
setSessionMode(selectedMode);
if (selectedMode === 'autopilot' || selectedMode === 'autopilot_fleet') {
setSessionMode('autopilot');
} else if (selectedMode === 'interactive' || selectedMode === 'exit_only') {
setSessionMode('interactive');
}
}
};
Expand Down Expand Up @@ -875,7 +873,7 @@ export function useOfficeChat(host: OfficeHostApp) {
const { toolCallId, toolName, arguments: args } = event.data;
// report_intent is an internal SDK tool — surface intent as thinking text
if (toolName === 'report_intent') {
const intent = (args as Record<string, unknown> | undefined)?.intent;
const intent = args?.intent;
if (typeof intent === 'string' && intent) {
// If tools have already been added, this intent starts a NEW phase
if (toolParts.size > 0) {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/websocket-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ export class BrowserCopilotSession {
this.toolHandlers.clear();
if (tools) {
for (const tool of tools) {
this.toolHandlers.set(tool.name, tool.handler);
if (tool.handler) {
this.toolHandlers.set(tool.name, tool.handler);
}
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/cliPluginBootstrap.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ export const OFFICE_CODING_AGENT_MARKETPLACE: OfficeCliPluginMarketplace;
export const REQUIRED_OFFICE_PLUGINS: string[];
export const REQUIRED_OFFICE_PLUGIN_SPECS: string[];

export function copilotHomeDir(): string;

export function getInstalledOfficePluginDirectories(options?: {
home?: string;
marketplace?: OfficeCliPluginMarketplace;
plugins?: string[];
fileExists?: (dir: string) => boolean;
}): string[];

export function runCopilotPluginCommand(
args: string[],
options?: { timeoutMs?: number; command?: string }
Expand Down
25 changes: 25 additions & 0 deletions src/plugins/cliPluginBootstrap.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { spawn } from 'node:child_process';
import { existsSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';

export const OFFICE_CODING_AGENT_MARKETPLACE = {
name: 'office-coding-agent',
Expand All @@ -12,6 +15,28 @@ export const REQUIRED_OFFICE_PLUGINS = [
'office-outlook',
];

/** Base directory the Copilot runtime uses for installed plugins (honors COPILOT_HOME). */
export function copilotHomeDir() {
return process.env.COPILOT_HOME || path.join(os.homedir(), '.copilot');
}

/**
* Absolute paths to the installed Office CLI plugin directories that exist on
* disk. The Copilot SDK (>=1.0) no longer auto-discovers marketplace-installed
* plugins for SDK-created sessions, so these directories must be passed to
* `createSession({ pluginDirectories })` for the plugin agents (e.g.
* `office-excel:excel`) to resolve.
*/
export function getInstalledOfficePluginDirectories(options = {}) {
const home = options.home ?? copilotHomeDir();
const marketplace = options.marketplace ?? OFFICE_CODING_AGENT_MARKETPLACE;
const plugins = options.plugins ?? REQUIRED_OFFICE_PLUGINS;
const fileExists = options.fileExists ?? existsSync;
return plugins
.map(name => path.join(home, 'installed-plugins', marketplace.name, name))
.filter(dir => fileExists(dir));
}

export const REQUIRED_OFFICE_PLUGIN_SPECS = REQUIRED_OFFICE_PLUGINS.map(
name => `${name}@${OFFICE_CODING_AGENT_MARKETPLACE.name}`
);
Expand Down
7 changes: 2 additions & 5 deletions src/services/skills/skillService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { AgentSkill, SkillMetadata } from '@/types/skill';
import type { SkillHost } from '@/types/skill';

/**
* Parse YAML frontmatter from a skill markdown file.
Expand Down Expand Up @@ -55,7 +54,7 @@ export function parseFrontmatter(raw: string): { metadata: SkillMetadata; conten
lower === 'word' ||
lower === 'outlook'
) {
metadata.hosts.push(lower as SkillHost);
metadata.hosts.push(lower);
}
}
continue;
Expand Down Expand Up @@ -91,9 +90,7 @@ export function parseFrontmatter(raw: string): { metadata: SkillMetadata; conten
} else if (currentKey === 'hosts') {
metadata.hosts = parseInlineArray(value)
.map(h => h.toLowerCase())
.filter(
h => h === 'excel' || h === 'powerpoint' || h === 'word' || h === 'outlook'
) as SkillHost[];
.filter(h => h === 'excel' || h === 'powerpoint' || h === 'word' || h === 'outlook');
} else {
setMetadataField(metadata, currentKey, value);
}
Expand Down
2 changes: 1 addition & 1 deletion src/stores/officeStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function getStorage(): OfficeRuntimeStorage {
if (typeof OfficeRuntime === 'undefined' || !OfficeRuntime?.storage) {
return localFallbackStorage();
}
return OfficeRuntime.storage as OfficeRuntimeStorage;
return OfficeRuntime.storage;
}

export const officeStorage: StateStorage = {
Expand Down
2 changes: 1 addition & 1 deletion src/tools/codegen/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { ToolConfig, ParamType } from './types';
interface ToolBinaryResult {
data: string;
mimeType: string;
type: string;
type: 'image' | 'resource';
description?: string;
}
import { getSheet } from '@/services/excel/helpers';
Expand Down
4 changes: 2 additions & 2 deletions src/tools/planner/wordPlanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function getLastDocumentPlan(): DocumentPlan | null {
return plan;
}

export const submitDocumentPlanTool: Tool = {
export const submitDocumentPlanTool = {
name: DOCUMENT_PLAN_TOOL_NAME,
description:
'Submit the structured document section plan. Call this exactly once with the complete plan for all sections.',
Expand Down Expand Up @@ -73,4 +73,4 @@ export const submitDocumentPlanTool: Tool = {
}
return `Document plan received: ${String(plan.sections?.length ?? 0)} sections.`;
},
};
} satisfies Tool;
2 changes: 2 additions & 0 deletions src/types/css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Allow side-effect imports of CSS files (handled by Vite/PostCSS at build time). */
declare module '*.css';
8 changes: 4 additions & 4 deletions tests-ui/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ function officeRuntimePolyfill() {
function makeSettingsJSON(overrides: Record<string, unknown> = {}) {
return JSON.stringify({
state: {
activeModel: 'claude-sonnet-4',
activeModel: 'claude-sonnet-4.6',
disabledMcpServerNames: [],
availableModels: [
{ id: 'claude-sonnet-4', name: 'Claude Sonnet 4', provider: 'Anthropic' },
{ id: 'gpt-4.1', name: 'GPT-4.1', provider: 'OpenAI' },
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'Google' },
{ id: 'claude-sonnet-4.6', name: 'Claude Sonnet 4.6', provider: 'Anthropic' },
{ id: 'gpt-5.4', name: 'GPT-5.4', provider: 'OpenAI' },
{ id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', provider: 'Google' },
],
...overrides,
},
Expand Down
4 changes: 2 additions & 2 deletions tests-ui/settings/settings-dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ test.describe('Model Picker', () => {

test('can select a different model', async ({ configuredTaskpane: page }) => {
await page.getByText('Claude Sonnet 4').click();
await page.getByText('GPT-4.1').click();
await page.getByText('GPT-5.4').click();

// Picker now shows the newly selected model
await expect(page.getByText('GPT-4.1')).toBeVisible({ timeout: 3000 });
await expect(page.getByText('GPT-5.4')).toBeVisible({ timeout: 3000 });
});
});
Loading
Loading