Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7671b34
wip
anj-s Jul 29, 2025
c13df02
Merge branch 'main' into u/anj/fix-divergence
anj-s Jul 30, 2025
d26577a
wip
anj-s Jul 30, 2025
17bb9f1
wip
anj-s Jul 30, 2025
3e89e1b
wip
anj-s Aug 1, 2025
d2271a0
wip, tool v1
anj-s Aug 1, 2025
8cd0bee
wip
anj-s Aug 1, 2025
a6afa06
wip
anj-s Aug 1, 2025
377487e
Merge branch 'main' into u/anj/write-todos
anj-s Aug 1, 2025
825624c
wip
anj-s Aug 1, 2025
fb5aa8d
wip
anj-s Aug 4, 2025
81e3c3e
Merge branch 'main' into u/anj/yolo-add-logging
anj-s Aug 5, 2025
b8d227e
wip
anj-s Aug 5, 2025
2af0220
wip
anj-s Aug 5, 2025
241c866
wip
anj-s Aug 6, 2025
7b46943
wip
anj-s Aug 6, 2025
64868ac
wip
anj-s Aug 6, 2025
e54f04e
wip
anj-s Aug 6, 2025
7f6337d
wip
anj-s Aug 6, 2025
90e5a76
wip
anj-s Aug 6, 2025
8ae51c6
wip
anj-s Aug 6, 2025
4941b10
wip
anj-s Aug 13, 2025
8fcf52c
wip
anj-s Aug 29, 2025
9073656
wip
anj-s Aug 29, 2025
75fec27
wip
anj-s Aug 29, 2025
e170f5a
wip
anj-s Sep 2, 2025
48c64a3
wip
anj-s Sep 3, 2025
94dcecf
wip
anj-s Sep 5, 2025
9977987
wip
anj-s Sep 10, 2025
86f35a4
wip
anj-s Sep 10, 2025
69fa62b
wip
anj-s Sep 13, 2025
4311a4c
wip
anj-s Sep 13, 2025
02cfccd
wip
anj-s Sep 17, 2025
9eaab48
Merge branch 'main' into u/anj/write-todos
anj-s Sep 17, 2025
53d6935
wip
anj-s Sep 18, 2025
d480048
Merge branch 'main' into u/anj/write-todos
anj-s Sep 18, 2025
4a423e9
wip
anj-s Sep 19, 2025
6531dd2
Merge branch 'main' into u/anj/write-todos
anj-s Sep 19, 2025
46bda4c
wip
anj-s Sep 19, 2025
a66eb62
Revert "feat(third_party) Port `get-ripgrep`." (#8923)
joshualitt Sep 19, 2025
4de9aca
Rollback shrinkwrap (#8926)
scidomino Sep 19, 2025
502da6f
Release: Ensure Tag Modification works (#8931)
mattKorwel Sep 19, 2025
5238f63
Update extension-releasing.md to have more info (#8927)
jakemac53 Sep 19, 2025
3363439
Add skip_github_release option to Manual Release. (#8932)
scidomino Sep 19, 2025
35e2f88
Add few more license file names to generate-notices script (#8939)
skeshive Sep 19, 2025
a2e8e3f
wip
anj-s Sep 20, 2025
b472252
Merge branch 'main' into u/anj/write-todos
anj-s Sep 20, 2025
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
2 changes: 2 additions & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface CliArgs {
includeDirectories: string[] | undefined;
screenReader: boolean | undefined;
useSmartEdit: boolean | undefined;
useWriteTodos: boolean | undefined;
promptWords: string[] | undefined;
outputFormat: string | undefined;
}
Expand Down Expand Up @@ -647,6 +648,7 @@ export async function loadCliConfig(
enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation,
eventEmitter: appEvents,
useSmartEdit: argv.useSmartEdit ?? settings.useSmartEdit,
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
output: {
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
},
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,15 @@ const SETTINGS_SCHEMA = {
description: 'Enable the smart-edit tool instead of the replace tool.',
showInDialog: false,
},
useWriteTodos: {
type: 'boolean',
label: 'Use Write Todos',
category: 'Advanced',
requiresRestart: false,
default: false,
description: 'Enable the write_todos_list tool.',
showInDialog: false,
},
security: {
type: 'object',
label: 'Security',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/gemini.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ describe('gemini.tsx main function kitty protocol', () => {
includeDirectories: undefined,
screenReader: undefined,
useSmartEdit: undefined,
useWriteTodos: undefined,
promptWords: undefined,
outputFormat: undefined,
});
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
import type { MCPOAuthConfig } from '../mcp/oauth-provider.js';
import { ideContextStore } from '../ide/ideContext.js';
import { WriteTodosTool } from '../tools/write-todos.js';
import type { FileSystemService } from '../services/fileSystemService.js';
import { StandardFileSystemService } from '../services/fileSystemService.js';
import {
Expand Down Expand Up @@ -245,6 +246,7 @@ export interface ConfigParameters {
enableToolOutputTruncation?: boolean;
eventEmitter?: EventEmitter;
useSmartEdit?: boolean;
useWriteTodos?: boolean;
policyEngineConfig?: PolicyEngineConfig;
output?: OutputSettings;
useModelRouter?: boolean;
Expand Down Expand Up @@ -332,6 +334,7 @@ export class Config {
private readonly fileExclusions: FileExclusions;
private readonly eventEmitter?: EventEmitter;
private readonly useSmartEdit: boolean;
private readonly useWriteTodos: boolean;
private readonly messageBus: MessageBus;
private readonly policyEngine: PolicyEngine;
private readonly outputSettings: OutputSettings;
Expand Down Expand Up @@ -421,6 +424,7 @@ export class Config {
this.enableToolOutputTruncation =
params.enableToolOutputTruncation ?? false;
this.useSmartEdit = params.useSmartEdit ?? true;
this.useWriteTodos = params.useWriteTodos ?? false;
this.useModelRouter = params.useModelRouter ?? false;
this.extensionManagement = params.extensionManagement ?? true;
this.storage = new Storage(this.targetDir);
Expand Down Expand Up @@ -947,6 +951,10 @@ export class Config {
return this.useSmartEdit;
}

getUseWriteTodos(): boolean {
return this.useWriteTodos;
}

getOutputFormat(): OutputFormat {
return this.outputSettings?.format
? this.outputSettings.format
Expand Down Expand Up @@ -1047,6 +1055,9 @@ export class Config {
registerCoreTool(ShellTool, this);
registerCoreTool(MemoryTool);
registerCoreTool(WebSearchTool, this);
if (this.getUseWriteTodos()) {
registerCoreTool(WriteTodosTool, this);
}

await registry.discoverAllTools();
return registry;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export * from './tools/web-search.js';
export * from './tools/read-many-files.js';
export * from './tools/mcp-client.js';
export * from './tools/mcp-tool.js';
export * from './tools/write-todos.js';

// MCP OAuth
export { MCPOAuthProvider } from './mcp/oauth-provider.js';
Expand Down
109 changes: 109 additions & 0 deletions packages/core/src/tools/write-todos.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it } from 'vitest';
import { WriteTodosTool, type WriteTodosToolParams } from './write-todos.js';

describe('WriteTodosTool', () => {
const tool = new WriteTodosTool();
const signal = new AbortController().signal;

describe('validation', () => {
it('should not throw for valid parameters', async () => {
const params: WriteTodosToolParams = {
todos: [
{ description: 'Task 1', status: 'pending' },
{ description: 'Task 2', status: 'in_progress' },
{ description: 'Task 3', status: 'completed' },
],
};
await expect(tool.buildAndExecute(params, signal)).resolves.toBeDefined();
});

it('should not throw for an empty list', async () => {
const params: WriteTodosToolParams = {
todos: [],
};
await expect(tool.buildAndExecute(params, signal)).resolves.toBeDefined();
});

it('should throw an error if todos is not an array', async () => {
const params = {
todos: 'not-an-array',
} as unknown as WriteTodosToolParams;
await expect(tool.buildAndExecute(params, signal)).rejects.toThrow(
'params/todos must be array',
);
});

it('should throw an error if a todo item is not an object', async () => {
const params = {
todos: ['not-an-object'],
} as unknown as WriteTodosToolParams;
await expect(tool.buildAndExecute(params, signal)).rejects.toThrow(
'params/todos/0 must be object',
);
});

it('should throw an error if a todo description is missing or empty', async () => {
const params: WriteTodosToolParams = {
todos: [{ description: ' ', status: 'pending' }],
};
await expect(tool.buildAndExecute(params, signal)).rejects.toThrow(
'Each todo must have a non-empty description string',
);
});

it('should throw an error if a todo status is invalid', async () => {
const params = {
todos: [{ description: 'Task 1', status: 'invalid-status' }],
} as unknown as WriteTodosToolParams;
await expect(tool.buildAndExecute(params, signal)).rejects.toThrow(
'params/todos/0/status must be equal to one of the allowed values',
);
});

it('should throw an error if more than one task is in_progress', async () => {
const params: WriteTodosToolParams = {
todos: [
{ description: 'Task 1', status: 'in_progress' },
{ description: 'Task 2', status: 'in_progress' },
],
};
await expect(tool.buildAndExecute(params, signal)).rejects.toThrow(
'Invalid parameters: Only one task can be "in_progress" at a time.',
);
});
});

describe('execute', () => {
it('should return a success message for clearing the list', async () => {
const params: WriteTodosToolParams = {
todos: [],
};
const result = await tool.buildAndExecute(params, signal);
expect(result.llmContent).toBe('Successfully cleared the todo list.');
expect(result.returnDisplay).toBe('Successfully cleared the todo list.');
});

it('should return a formatted todo list on success', async () => {
const params: WriteTodosToolParams = {
todos: [
{ description: 'First task', status: 'completed' },
{ description: 'Second task', status: 'in_progress' },
{ description: 'Third task', status: 'pending' },
],
};
const result = await tool.buildAndExecute(params, signal);
const expectedOutput = `Successfully updated the todo list. The current list is now:
1. [completed] First task
2. [in_progress] Second task
3. [pending] Third task`;
expect(result.llmContent).toBe(expectedOutput);
expect(result.returnDisplay).toBe(expectedOutput);
});
});
});
Loading