1. ๆžถๆž„ๆฆ‚่งˆ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     opencode Todo ็ณป็ปŸๆžถๆž„                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                  โ”‚
โ”‚   AI Agent                                                       โ”‚
โ”‚      โ”‚                                                           โ”‚
โ”‚      โ–ผ                                                           โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                           โ”‚
โ”‚   โ”‚ todoread    โ”‚     โ”‚ todowrite   โ”‚  โ† ไธคไธชๆž็ฎ€ๅทฅๅ…ท           โ”‚
โ”‚   โ”‚ (ๆ— ๅ‚ๆ•ฐ)    โ”‚     โ”‚ (todos[])   โ”‚                           โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜                           โ”‚
โ”‚          โ”‚                   โ”‚                                   โ”‚
โ”‚          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                   โ”‚
โ”‚                    โ–ผ                                             โ”‚
โ”‚            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                     โ”‚
โ”‚            โ”‚  Todo Module  โ”‚                                     โ”‚
โ”‚            โ”‚  (todo.ts)    โ”‚                                     โ”‚
โ”‚            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                     โ”‚
โ”‚                    โ”‚                                             โ”‚
โ”‚         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                   โ”‚
โ”‚         โ–ผ         โ–ผ         โ–ผ                                   โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                              โ”‚
โ”‚      โ”‚ get โ”‚  โ”‚updateโ”‚  โ”‚ Event  โ”‚  โ† Bus ไบ‹ไปถๅ‘ๅธƒ              โ”‚
โ”‚      โ””โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                              โ”‚
โ”‚         โ”‚        โ”‚          โ”‚                                    โ”‚
โ”‚         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                  โ–ผ                                               โ”‚
โ”‚         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚         โ”‚  SQLite       โ”‚                                        โ”‚
โ”‚         โ”‚  TodoTable    โ”‚                                        โ”‚
โ”‚         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚                  โ”‚                                               โ”‚
โ”‚                  โ–ผ                                               โ”‚
โ”‚         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚         โ”‚   TUI Sidebar โ”‚  โ† ๅฎžๆ—ถ่ฎข้˜…ๆ˜พ็คบ                        โ”‚
โ”‚         โ”‚  (sidebar.tsx)โ”‚                                        โ”‚
โ”‚         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚                                                                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๆ ธๅฟƒ่ฎพ่ฎกๅŽŸๅˆ™๏ผš

  • ๆž็ฎ€ๆŽฅๅฃ๏ผšไป…ไธคไธชๅทฅๅ…ท๏ผˆread/write๏ผ‰๏ผŒๆ— ๅขž้‡ๆ›ดๆ–ฐ
  • ๅ…จ้‡ๆ›ฟๆข๏ผšๆฏๆฌกๆ›ดๆ–ฐไผ ๅ…ฅๅฎŒๆ•ดๅˆ—่กจ๏ผŒ็ฎ€ๅŒ–ๅนถๅ‘ๆŽงๅˆถ
  • Session ้š”็ฆป๏ผšๆฏไธชไผš่ฏ็‹ฌ็ซ‹ๅญ˜ๅ‚จ๏ผŒๅคฉ็„ถๆ”ฏๆŒๅคšไผš่ฏ
  • ไบ‹ไปถ้ฉฑๅŠจ๏ผš้€š่ฟ‡ Bus ไบ‹ไปถๅฎž็Žฐ UI ๅฎžๆ—ถๅŒๆญฅ

โš ๏ธ ้‡่ฆๅŒบๅˆซ๏ผšTodo ๅทฅๅ…ท vs Plan ๆจกๅผ

็‰นๆ€งTodo ๅทฅๅ…ทPlan ๆจกๅผ
่งฆๅ‘ๆ–นๅผtodowrite ๅทฅๅ…ท่ฐƒ็”จplan_enter ๅทฅๅ…ท่ฐƒ็”จ
Agent ๅˆ‡ๆขโŒ ไธๅˆ‡ๆข๏ผŒๅฝ“ๅ‰ agent ็ปง็ปญไฝฟ็”จโœ… ไผšๅˆ‡ๆข build โ†’ plan
ๅทฅๅ…ทๆƒ้™ๅ˜ๅŒ–ๆ— ๅ˜ๅŒ–Plan agent ้™ๅˆถ็ผ–่พ‘ๆƒ้™
็ณป็ปŸๆ้†’ๆ— ๆณจๅ…ฅ PLAN_MODE_REMINDER
็›ฎ็š„ไปปๅŠก่ทŸ่ธช็ฎก็†่ฟ›ๅ…ฅๅช่ฏป่ง„ๅˆ’้˜ถๆฎต

ๅ…ณ้”ฎ็†่งฃ๏ผšTodo ๆ˜ฏๅŒไธ€ Agent ๅ†…็š„ไปปๅŠก็ฎก็†ๅทฅๅ…ท๏ผŒPlan ๆ˜ฏไธๅŒ Agent ไน‹้—ด็š„ๆจกๅผๅˆ‡ๆขใ€‚


2. ๆ•ฐๆฎๆจกๅž‹

2.1 TypeScript ๅฎšไน‰

ๆ–‡ไปถ: packages/opencode/src/session/todo.ts

export namespace Todo {
  // ๆ ธๅฟƒๆ•ฐๆฎๆจกๅž‹ - ๅชๆœ‰ 3 ไธชๅญ—ๆฎต
  export const Info = z
    .object({
      content: z.string().describe("Brief description of the task"),
      status: z.string().describe("Current status: pending, in_progress, completed, cancelled"),
      priority: z.string().describe("Priority level: high, medium, low"),
    })
    .meta({ ref: "Todo" })
 
  export type Info = z.infer<typeof Info>
}

2.2 ๅญ—ๆฎต่ฏดๆ˜Ž

ๅญ—ๆฎต็ฑปๅž‹่ฏดๆ˜Ž็คบไพ‹ๅ€ผ
contentstringไปปๅŠกๆ่ฟฐโ€Implement user authenticationโ€
statusstringไปปๅŠก็Šถๆ€"pending", "in_progress", "completed", "cancelled"
prioritystringไผ˜ๅ…ˆ็บง"high", "medium", "low"

ไธบไป€ไนˆๆฒกๆœ‰ๆ›ดๅคšๅญ—ๆฎต๏ผŸ

  • ๆฒกๆœ‰ id๏ผš็”จๆ•ฐ็ป„็ดขๅผ•๏ผˆposition๏ผ‰ๆ ‡่ฏ†
  • ๆฒกๆœ‰ created_at/updated_at๏ผš็ฎ€ๅŒ–่ฎพ่ฎก๏ผŒไธ้œ€่ฆๆ—ถ้—ด่ฟฝ่ธช
  • ๆฒกๆœ‰ assignee๏ผšๅ•็”จๆˆทๅœบๆ™ฏ๏ผŒๆ— ้œ€ๅˆ†้…

3. ๅญ˜ๅ‚จๅฑ‚ๅฎž็Žฐ

3.1 ๆ•ฐๆฎๅบ“่กจ็ป“ๆž„

ๆ–‡ไปถ: packages/opencode/src/session/session.sql.ts

export const TodoTable = sqliteTable(
  "todo",
  {
    session_id: text()
      .notNull()
      .references(() => SessionTable.id, { onDelete: "cascade" }),
    content: text().notNull(),
    status: text().notNull(),     // pending | in_progress | completed | cancelled
    priority: text().notNull(),   // high | medium | low
    position: integer().notNull(), // ็”จไบŽๆŽ’ๅบ
  },
  (table) => [
    // ๅคๅˆไธป้”ฎ๏ผš(session_id, position)
    primaryKey({ columns: [table.session_id, table.position] }),
    // ็ดขๅผ•ๅŠ ้€ŸๆŸฅ่ฏข
    index("todo_session_idx").on(table.session_id),
  ],
)

่ฎพ่ฎก็‰น็‚น๏ผš

็‰นๆ€ง่ฏดๆ˜Žไผ˜ๅŠฟ
Session ็บง่”ๅˆ ้™คonDelete: "cascade"ๅˆ ้™ค session ่‡ชๅŠจๆธ…็† todos
ๅคๅˆไธป้”ฎ(session_id, position)็กฎไฟๆฏไธช session ๅ†…้กบๅบๅ”ฏไธ€
position ๅญ—ๆฎตๆ•ฐ็ป„็ดขๅผ•ๆ”ฏๆŒๆŽ’ๅบ๏ผŒๆ— ้œ€้ขๅค–็š„ order ๅญ—ๆฎต

3.2 ๅญ˜ๅ‚จๆ“ไฝœ

ๆ–‡ไปถ: packages/opencode/src/session/todo.ts

export namespace Todo {
  // ๅ…จ้‡ๆ›ฟๆขๆ›ดๆ–ฐ - ๅ…ณ้”ฎ่ฎพ่ฎก๏ผ
  export function update(input: { sessionID: string; todos: Info[] }) {
    Database.transaction((db) => {
      // 1. ๅˆ ้™ค่ฏฅ session ็š„ๆ‰€ๆœ‰ todos
      db.delete(TodoTable)
        .where(eq(TodoTable.session_id, input.sessionID))
        .run()
 
      if (input.todos.length === 0) return
 
      // 2. ๆ’ๅ…ฅๆ–ฐๅˆ—่กจ๏ผˆposition ๅญ—ๆฎตๆŽงๅˆถ้กบๅบ๏ผ‰
      db.insert(TodoTable)
        .values(
          input.todos.map((todo, position) => ({
            session_id: input.sessionID,
            content: todo.content,
            status: todo.status,
            priority: todo.priority,
            position,
          })),
        )
        .run()
    })
 
    // 3. ๅ‘ๅธƒๆ›ดๆ–ฐไบ‹ไปถ๏ผŒTUI ่ฎข้˜…ๆญคไบ‹ไปถ
    Bus.publish(Event.Updated, input)
  }
 
  // ๆŸฅ่ฏข - ๆŒ‰ position ๆŽ’ๅบ
  export function get(sessionID: string) {
    const rows = Database.use((db) =>
      db.select()
        .from(TodoTable)
        .where(eq(TodoTable.session_id, sessionID))
        .orderBy(asc(TodoTable.position))
        .all(),
    )
    return rows.map((row) => ({
      content: row.content,
      status: row.status,
      priority: row.priority,
    }))
  }
}

ไธบไป€ไนˆ็”จๅ…จ้‡ๆ›ฟๆข๏ผŸ

  1. ็ฎ€ๅŒ–ๅนถๅ‘๏ผš้ฟๅ…ๅคๆ‚็š„ merge ้€ป่พ‘
  2. AI ๅ‹ๅฅฝ๏ผšLLM ็”ŸๆˆๅฎŒๆ•ดๅˆ—่กจๆฏ”่ฎก็ฎ— diff ๆ›ดๅฎนๆ˜“
  3. ๅน‚็ญ‰ๆ€ง๏ผšๅคšๆฌกๆ‰ง่กŒ็›ธๅŒ่ฏทๆฑ‚็ป“ๆžœไธ€่‡ด
  4. ไบ‹ๅŠกๅฎ‰ๅ…จ๏ผšๅ•ๆฌกไบ‹ๅŠกๅฎŒๆˆ๏ผŒไธไผš้ƒจๅˆ†ๆ›ดๆ–ฐ

4. ๅทฅๅ…ทๅฑ‚ๅฎž็Žฐ

4.0 Todo vs Plan๏ผšๅ…ณ้”ฎๅŒบๅˆซ

ๅพˆๅคšๅผ€ๅ‘่€…ๅฎนๆ˜“ๆททๆท† Todo ๅทฅๅ…ท ๅ’Œ Plan ๆจกๅผ๏ผŒๅฎƒไปฌๆ˜ฏๅฎŒๅ…จไธๅŒ็š„ๆœบๅˆถ๏ผš

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    ไธค็งๅคๆ‚ๅบฆ็ฎก็†ๆ–นๅผๅฏนๆฏ”                        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  Plan ๆจกๅผ (Agent ๅˆ‡ๆข)                                  โ”‚   โ”‚
โ”‚  โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                                   โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ่งฆๅ‘๏ผš่ฐƒ็”จ plan_enter ๅทฅๅ…ท                            โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ็ป“ๆžœ๏ผšๅˆ‡ๆขๅˆฐ Plan Agent                               โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ๆƒ้™๏ผš่ฟ›ๅ…ฅๅช่ฏปๆจกๅผ๏ผŒไธ่ƒฝ็ผ–่พ‘ไปฃ็ ๆ–‡ไปถ                  โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ็›ฎ็š„๏ผš่ฐƒ็ ”ๅˆ†ๆž๏ผŒๅˆถๅฎš่ง„ๅˆ’๏ผŒๅ†™ๅ…ฅ .dm_cc/plans/*.md      โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ้€€ๅ‡บ๏ผš่ฐƒ็”จ plan_exit ๅˆ‡ๆขๅ›ž Build Agent               โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  Todo ๅทฅๅ…ท (ๅŒไธ€ Agent ๅ†…)                               โ”‚   โ”‚
โ”‚  โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                                โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ่งฆๅ‘๏ผš่ฐƒ็”จ todowrite ๅทฅๅ…ท                             โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ็ป“ๆžœ๏ผšๅฝ“ๅ‰ Agent ็ปง็ปญๆ‰ง่กŒ๏ผˆไธๅˆ‡ๆข๏ผ‰                   โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ๆƒ้™๏ผšๆ— ๅ˜ๅŒ–๏ผŒBuild Agent ไปๅฏไฝฟ็”จๆ‰€ๆœ‰ๅทฅๅ…ท            โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ็›ฎ็š„๏ผš่ทŸ่ธชไปปๅŠก่ฟ›ๅบฆ๏ผŒๆ ‡่ฎฐๅฎŒๆˆ็Šถๆ€                      โ”‚   โ”‚
โ”‚  โ”‚  โ€ข ๅญ˜ๅ‚จ๏ผš.dm_cc/todos/{session_id}.json                  โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๅ†ณ็ญ–ๆต็จ‹๏ผš

็”จๆˆท่ฏทๆฑ‚ๅคๆ‚ไปปๅŠก
       โ”‚
       โ”œโ”€โ”€ ้œ€่ฆๅ…ˆ่ฐƒ็ ”/่ง„ๅˆ’๏ผŸโ”€โ”€โ”€โ–บ ่ฐƒ็”จ plan_enter โ”€โ”€โ–บ ่ฟ›ๅ…ฅ Plan Agent
       โ”‚                         (ๅˆถๅฎšๆ•ดไฝ“ๆ–นๆกˆ)
       โ”‚
       โ””โ”€โ”€ ็›ดๆŽฅๆ‰ง่กŒๅคšๆญฅ้ชคไปปๅŠก๏ผŸโ”€โ”€โ–บ ่ฐƒ็”จ todowrite โ”€โ”€โ–บ ไฟๆŒ Build Agent
                                   (่ทŸ่ธชๆ‰ง่กŒ่ฟ›ๅบฆ)

4.1 ๅทฅๅ…ทๅฎšไน‰

ๆ–‡ไปถ: packages/opencode/src/tool/todo.ts

export const TodoWriteTool = Tool.define("todowrite", {
  description: DESCRIPTION_WRITE,  // ่ฏฆ็ป†็š„ few-shot ๆ่ฟฐ
  parameters: z.object({
    todos: z.array(z.object(Todo.Info.shape))
      .describe("The updated todo list"),
  }),
  async execute(params, ctx) {
    // ๆƒ้™ๆฃ€ๆŸฅ๏ผˆๅฏ้€‰๏ผŒๅ› ไธบ always: ["*"]๏ผ‰
    await ctx.ask({
      permission: "todowrite",
      patterns: ["*"],
      always: ["*"],
      metadata: {},
    })
 
    await Todo.update({
      sessionID: ctx.sessionID,
      todos: params.todos,
    })
 
    return {
      title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
      output: JSON.stringify(params.todos, null, 2),
      metadata: { todos: params.todos },
    }
  },
})
 
export const TodoReadTool = Tool.define("todoread", {
  description: "Use this tool to read your todo list",
  parameters: z.object({}),  // ๆ— ๅ‚ๆ•ฐ
  async execute(_params, ctx) {
    await ctx.ask({
      permission: "todoread",
      patterns: ["*"],
      always: ["*"],
      metadata: {},
    })
 
    const todos = await Todo.get(ctx.sessionID)
    return {
      title: `${todos.filter((x) => x.status !== "completed").length} todos`,
      metadata: { todos },
      output: JSON.stringify(todos, null, 2),
    }
  },
})

4.2 ๅทฅๅ…ทๅ‚ๆ•ฐๅฏนๆฏ”

ๅทฅๅ…ทๅ‚ๆ•ฐ่ฟ”ๅ›žๅ€ผ
todoreadๆ— ๏ผˆ็ฉบๅฏน่ฑก๏ผ‰{ title, output, metadata: { todos } }
todowrite{ todos: Todo.Info[] }{ title, output, metadata: { todos } }

title ็š„่ฎก็ฎ—๏ผšpending ็Šถๆ€็š„ไปปๅŠกๆ•ฐ้‡

title: `${todos.filter((x) => x.status !== "completed").length} todos`

4.3 ๅทฅๅ…ทๆ่ฟฐๆ–‡ไปถ

ๆ–‡ไปถ: packages/opencode/src/tool/todowrite.txt๏ผˆ่Š‚้€‰๏ผ‰

Use this tool to create and manage a structured task list for your current coding session.
This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.

## When to Use This Tool

Use this tool proactively in these scenarios:

1. Complex multistep tasks - When a task requires 3 or more distinct steps or actions
2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
3. User explicitly requests todo list - When the user directly asks you to use the todo list
4. User provides multiple tasks - When users provide a list of things to be done
5. After receiving new instructions - Immediately capture user requirements as todos
6. After completing a task - Mark it complete and add any new follow-up tasks
7. When you start working on a new task, mark the todo as in_progress

## When NOT to Use This Tool

Skip using this tool when:
1. There is only a single, straightforward task
2. The task is trivial and tracking it provides no organizational benefit
3. The task can be completed in less than 3 trivial steps
4. The task is purely conversational or informational

## Task States and Management

1. **Task States**:
   - pending: Task not yet started
   - in_progress: Currently working on (limit to ONE task at a time)
   - completed: Task finished successfully
   - cancelled: Task no longer needed

2. **Task Management**:
   - Update task status in real-time as you work
   - Mark tasks complete IMMEDIATELY after finishing
   - Only have ONE task in_progress at any time
   - Complete current tasks before starting new ones

4.4 ่งฆๅ‘ๆกไปถๅˆ†ๆž๏ผˆไปŽ Prompt ๆๅ–๏ผ‰

ไป€ไนˆๆ—ถๅ€™่ฐƒ็”จ todowrite๏ผŸ โ€”โ€” ๅฎŒๅ…จ็”ฑ Prompt ่ง„ๅˆ™ๅ†ณๅฎš๏ผŒไธๆ˜ฏไปฃ็ ็กฌ็ผ–็ ๏ผš

ๅœบๆ™ฏ่งฆๅ‘ๆกไปถ็คบไพ‹
โœ… ๅคๆ‚ๅคšๆญฅ้ชคไปปๅŠก้œ€่ฆ 3+ ไธชไธๅŒๆญฅ้ชคๅฎž็Žฐๆš—้ป‘ๆจกๅผ๏ผˆUI + ็Šถๆ€็ฎก็† + ๆ ทๅผ๏ผ‰
โœ… ็”จๆˆทๆ˜Ž็กฎ่ฆๆฑ‚็”จๆˆท่ฏด โ€œcreate a todo list""ๅธฎๆˆ‘ๅˆ›ๅปบไธ€ไธชไปปๅŠกๅˆ—่กจ่ทŸ่ธช่ฟ›ๅบฆโ€
โœ… ๅคšไธชไปปๅŠก็”จๆˆทๆไพ›ๅˆ—่กจ๏ผˆ้€—ๅท/็ผ–ๅทๅˆ†้š”๏ผ‰โ€œๅฎž็Žฐ A, B, C ไธ‰ไธชๅŠŸ่ƒฝโ€
โœ… ๆ–ฐๆŒ‡ไปคๆ”ถๅˆฐๆ–ฐ้œ€ๆฑ‚ๆ—ถ็ซ‹ๅณๆ•่Žท็”จๆˆทไธญ้€”่ฟฝๅŠ ้œ€ๆฑ‚
โœ… ๅผ€ๅง‹ๆ–ฐไปปๅŠกๅฐ†ๆ–ฐไปปๅŠกๆ ‡่ฎฐไธบ in_progressๅฎŒๆˆ็ฌฌไธ€ๆญฅ๏ผŒๅผ€ๅง‹็ฌฌไบŒๆญฅ
โŒ ็ฎ€ๅ•ไปปๅŠก< 3 ไธชๆญฅ้ชค๏ผŒ็›ดๆŽฅๅฎŒๆˆโ€print Hello Worldโ€
โŒ ็บฏๅฏน่ฏไฟกๆฏๆŸฅ่ฏข๏ผŒๆ— ๅฎž้™…ๆ“ไฝœโ€git status ๆ˜ฏๅšไป€ไนˆ็š„๏ผŸโ€

ๅ…ณ้”ฎ็†่งฃ๏ผš

  • ไฝฟ็”จ Todo ไธไผš่งฆๅ‘ไปปไฝ•็Šถๆ€ๅ˜ๅŒ–ๆˆ– Agent ๅˆ‡ๆข
  • ๅชๆ˜ฏๆ™ฎ้€š็š„ๅทฅๅ…ท่ฐƒ็”จ๏ผŒๆ‰ง่กŒๅŽ่ฟ”ๅ›ž็ป“ๆžœ๏ผŒ็ปง็ปญๅฝ“ๅ‰ Agent ็š„ๅพช็Žฏ
  • ไธŽ Plan ๆจกๅผ็š„ๅŒบๅˆซ๏ผšPlan ไผšๅˆ‡ๆข Agent ๅนถๆ”นๅ˜็ณป็ปŸ prompt

5. ๅฎž้™…ไฝฟ็”จ็คบไพ‹

5.1 ็คบไพ‹ 1: ๆทปๅŠ ๆš—้ป‘ๆจกๅผ

็”จๆˆท: โ€œI want to add a dark mode toggle to the application settings. Make sure you run the tests and build when youโ€™re done!โ€

AI ๆ‰ง่กŒๆต็จ‹:

// Step 1: ๅˆ›ๅปบ todo ๅˆ—่กจ
await todowrite({
  todos: [
    { content: "Create dark mode toggle component in Settings page", status: "pending", priority: "high" },
    { content: "Add dark mode state management (context/store)", status: "pending", priority: "high" },
    { content: "Implement CSS-in-JS styles for dark theme", status: "pending", priority: "medium" },
    { content: "Update existing components to support theme switching", status: "pending", priority: "medium" },
    { content: "Run tests and build process", status: "pending", priority: "high" },
  ]
})
 
// Step 2: ๅผ€ๅง‹็ฌฌไธ€ไธชไปปๅŠก
await todowrite({
  todos: [
    { content: "Create dark mode toggle component in Settings page", status: "in_progress", priority: "high" },
    // ... ๅ…ถไป–ไฟๆŒไธๅ˜
  ]
})
 
// Step 3: ๅฎŒๆˆ็ฌฌไธ€ไธชไปปๅŠก๏ผŒๅผ€ๅง‹็ฌฌไบŒไธช
await todowrite({
  todos: [
    { content: "Create dark mode toggle component in Settings page", status: "completed", priority: "high" },
    { content: "Add dark mode state management (context/store)", status: "in_progress", priority: "high" },
    // ... ๅ…ถไป–ไฟๆŒไธๅ˜
  ]
})

5.2 ็คบไพ‹ 2: ้‡ๆž„ๅ‡ฝๆ•ฐๅ

็”จๆˆท: โ€œHelp me rename the function getCwd to getCurrentWorkingDirectory across my projectโ€

AI ๆ‰ง่กŒๆต็จ‹:

// Step 1: ๅ…ˆๆœ็ดขไบ†่งฃๆƒ…ๅ†ต
const results = await search({ pattern: "getCwd" })
// ๅ‘็Žฐ 15 ๅค„่ฐƒ็”จ๏ผŒๅˆ†ๅธƒๅœจ 8 ไธชๆ–‡ไปถไธญ
 
// Step 2: ๅˆ›ๅปบ todo ๅˆ—่กจ
await todowrite({
  todos: [
    { content: "Update getCwd in src/utils/path.ts", status: "pending", priority: "high" },
    { content: "Update getCwd in src/config/loader.ts", status: "pending", priority: "high" },
    { content: "Update getCwd in src/cli/commands.ts", status: "pending", priority: "high" },
    { content: "Update getCwd in tests/path.test.ts", status: "pending", priority: "medium" },
    // ... ๅ…ถไป–ๆ–‡ไปถ
  ]
})
 
// Step 3: ้€ไธชๅค„็†...

5.3 ็คบไพ‹ 3: ่ฏปๅ–ๅนถๆ›ดๆ–ฐ

// ่ฏปๅ–ๅฝ“ๅ‰ๅˆ—่กจ
const { metadata } = await todoread({})
const currentTodos = metadata.todos
 
// ๆทปๅŠ ๆ–ฐไปปๅŠก
await todowrite({
  todos: [
    ...currentTodos,
    { content: "New follow-up task", status: "pending", priority: "medium" }
  ]
})

6. UI ๅฑ•็คบ

6.1 TUI ไพง่พนๆ ๅฑ•็คบ

ๆ–‡ไปถ: packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx

<Show when={todo().length > 0 && todo().some((t) => t.status !== "completed")}>
  <box>
    <box flexDirection="row">
      <text><b>Todo</b></text>
    </box>
    <For each={todo()}>
      {(todo) => <TodoItem status={todo.status} content={todo.content} />}
    </For>
  </box>
</Show>

ๆ˜พ็คบๆกไปถ๏ผš

  1. ๆœ‰ todo ้กน
  2. ่‡ณๅฐ‘ๆœ‰ไธ€ไธชๆœชๅฎŒๆˆ็š„ไปปๅŠก

6.2 Todo ้กน็ป„ไปถ

ๆ–‡ไปถ: packages/opencode/src/cli/cmd/tui/component/todo-item.tsx

export function TodoItem(props: { status: string; content: string }) {
  return (
    <box flexDirection="row">
      {/* ็Šถๆ€ๅ›พๆ ‡ */}
      <text style={{
        fg: props.status === "in_progress" ? theme.warning : theme.textMuted,
      }}>
        [{props.status === "completed" ? "โœ“" :
           props.status === "in_progress" ? "โ€ข" : " "}] {" "}
      </text>
 
      {/* ไปปๅŠกๅ†…ๅฎน */}
      <text style={{
        fg: props.status === "in_progress" ? theme.warning : theme.textMuted,
      }}>
        {props.content}
      </text>
    </box>
  )
}

6.3 UI ๆ•ˆๆžœ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Session: ๅฎž็Žฐ็”ตๅ•†็ฝ‘็ซ™                  โ”‚
โ”‚                                         โ”‚
โ”‚  Todo                                   โ”‚
โ”‚  [โ€ข] ่ฎพ่ฎกๅ•†ๅ“ๅฑ•็คบ้กต้ข  โ† in_progress    โ”‚
โ”‚  [ ] ๅฎž็Žฐ่ดญ็‰ฉ่ฝฆๅŠŸ่ƒฝ    โ† pending        โ”‚
โ”‚  [ ] ๅผ€ๅ‘่ฎขๅ•็ฎก็†็ณป็ปŸ                   โ”‚
โ”‚  [โœ“] ๅˆๅง‹ๅŒ–้กน็›ฎ็ป“ๆž„   โ† completed       โ”‚
โ”‚                                         โ”‚
โ”‚  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€                  โ”‚
โ”‚  Cost: $0.023                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

่ง†่ง‰่ฎพ่ฎก๏ผš

  • โœ“ - ๅทฒๅฎŒๆˆ๏ผˆ็ฐ่‰ฒ/ๆŸ”ๅ’Œ๏ผ‰
  • โ€ข - ่ฟ›่กŒไธญ๏ผˆ้ป„่‰ฒ/่ญฆๅ‘Š่‰ฒ๏ผŒ็ชๅ‡บๆ˜พ็คบ๏ผ‰
  • - ๅพ…ๅค„็†๏ผˆ็ฐ่‰ฒ๏ผ‰

7. ไธŽ dm_cc ็š„ๅฏนๆฏ”

7.1 ๆžถๆž„ๅฏนๆฏ”

็‰นๆ€งopencode (TypeScript)dm_cc ๅปบ่ฎฎ (Python)
ๅญ˜ๅ‚จSQLite + DrizzleJSON ๆ–‡ไปถ / SQLite
ไบ‹ไปถๆ€ป็บฟ่‡ชๅฎšไน‰ Busๆ— ้œ€๏ผˆๆ—  TUI๏ผ‰
UI ๆก†ๆžถInk (React-like)ๆ—  / Rich Console
ๆƒ้™PermissionNext ่ง„ๅˆ™AgentConfig ๅทฅๅ…ท่ฟ‡ๆปค
ๆ›ดๆ–ฐ็ญ–็•ฅๅ…จ้‡ๆ›ฟๆขๅ…จ้‡ๆ›ฟๆข๏ผˆไฟๆŒไธ€่‡ด๏ผ‰

7.2 ไปฃ็ ๅฏนๆฏ”

ๆ•ฐๆฎๆจกๅž‹:

// opencode - TypeScript
export const Info = z.object({
  content: z.string(),
  status: z.string(),   // pending | in_progress | completed | cancelled
  priority: z.string(), // high | medium | low
})
# dm_cc - Python ๅปบ่ฎฎ
@dataclass
class TodoItem:
    content: str
    status: Literal["pending", "in_progress", "completed", "cancelled"]
    priority: Literal["high", "medium", "low"]

ๅญ˜ๅ‚จ:

// opencode - SQLite
export function update(input: { sessionID: string; todos: Info[] }) {
  Database.transaction((db) => {
    db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run()
    if (input.todos.length === 0) return
    db.insert(TodoTable).values(...).run()
  })
  Bus.publish(Event.Updated, input)
}
# dm_cc - JSON ๆ–‡ไปถๅปบ่ฎฎ
class TodoStore:
    def update(self, todos: list[TodoItem]) -> None:
        """ๅ…จ้‡ๆ›ฟๆขๆ›ดๆ–ฐ"""
        data = [todo.to_dict() for todo in todos]
        self._file_path.write_text(
            json.dumps(data, indent=2, ensure_ascii=False)
        )

8. ่ฎพ่ฎก่ฆ็‚นๆ€ป็ป“

8.1 ๅ…ณ้”ฎ่ฎพ่ฎกๅ†ณ็ญ–

ๅ†ณ็ญ–้€‰ๆ‹ฉ็†็”ฑ
ๆ›ดๆ–ฐ็ญ–็•ฅๅ…จ้‡ๆ›ฟๆข้ฟๅ…ๅคๆ‚ๅนถๅ‘ๆŽงๅˆถ๏ผŒAI ๅ‹ๅฅฝ

8.2 Agent ๆƒ้™้…็ฝฎ

ๆ–‡ไปถ: packages/opencode/src/agent/agent.ts

const result: Record<string, Info> = {
  build: {
    name: "build",
    // Build agent ้ป˜่ฎคๅฏ็”จๆ‰€ๆœ‰ๅทฅๅ…ท๏ผŒๅŒ…ๆ‹ฌ todowrite/todoread
    permission: PermissionNext.merge(defaults, {
      question: "allow",
      plan_enter: "allow",
      // todowrite/todoread ้ป˜่ฎคๅฏ็”จ๏ผˆๅœจ defaults ไธญ "*": "allow"๏ผ‰
    }),
    mode: "primary",
  },
  plan: {
    name: "plan",
    // Plan agent ไนŸๅฏไปฅไฝฟ็”จ todo ๅทฅๅ…ท
    permission: PermissionNext.merge(defaults, {
      plan_exit: "allow",
      // ๆฒกๆœ‰็ฆ็”จ todo๏ผŒ่ง„ๅˆ’ๆ—ถไนŸ้œ€่ฆไปปๅŠก็ฎก็†
    }),
    mode: "primary",
  },
  general: {
    name: "general",
    // Subagent - ๆ˜พๅผ็ฆ็”จ todo๏ผ
    permission: PermissionNext.merge(defaults, {
      todoread: "deny",   // โŒ ็ฆๆญข
      todowrite: "deny",  // โŒ ็ฆๆญข
    }),
    mode: "subagent",
  },
  explore: {
    name: "explore",
    // Explore subagent ไนŸ็ฆ็”จ todo
    permission: PermissionNext.merge(defaults, {
      todoread: "deny",
      todowrite: "deny",
    }),
    mode: "subagent",
  },
}

ไธบไป€ไนˆ Subagent ็ฆ็”จ Todo๏ผŸ

  • Subagent๏ผˆgeneral/explore๏ผ‰ๅชๆ‰ง่กŒๅ•ไธ€ไปปๅŠก
  • ไธๅบ”่ฎฟ้—ฎๆˆ–ไฟฎๆ”น็ˆถไผš่ฏ็š„ todo ๅˆ—่กจ
  • ไฟๆŒๅ…ณๆณจ็‚นๅˆ†็ฆป๏ผŒ้ฟๅ…ๅญไปปๅŠกๅนฒๆ‰ฐไธปไปปๅŠก่ทŸ่ธช

8.3 Todo ไธŽ Plan ็š„ๅฎŒๆ•ดๅฏนๆฏ”

็ปดๅบฆTodo ๅทฅๅ…ทPlan ๆจกๅผ
่งฆๅ‘ๆ–นๅผtodowrite ๅทฅๅ…ท่ฐƒ็”จplan_enter ๅทฅๅ…ท่ฐƒ็”จ
Agent ๅˆ‡ๆขโŒ ๅฆ๏ผŒไฟๆŒๅฝ“ๅ‰ Agentโœ… ๆ˜ฏ๏ผŒbuild โ†’ plan
ๆƒ้™ๅ˜ๅŒ–ๆ— Plan agent ้™ๅˆถ็ผ–่พ‘ๆƒ้™
็ณป็ปŸ Promptไธๅ˜ๆณจๅ…ฅ PLAN_MODE_REMINDER
็›ฎ็š„่ทŸ่ธชไปปๅŠกๆ‰ง่กŒ่ฟ›ๅบฆ่ฟ›ๅ…ฅๅช่ฏป่ง„ๅˆ’้˜ถๆฎต
ๅญ˜ๅ‚จไฝ็ฝฎ.dm_cc/todos/{session_id}.json.dm_cc/plans/*.md
้€‚็”จ AgentBuild, PlanBuild ่ฐƒ็”จ๏ผŒPlan ๆ‰ง่กŒ
Subagent ๆƒ้™โŒ general/explore ็ฆ็”จโŒ general/explore ็ฆ็”จ
ๅ‚ๆ•ฐ่ฎพ่ฎกๆž็ฎ€๏ผˆtodos ๆ•ฐ็ป„๏ผ‰LLM ๅฎนๆ˜“็”ŸๆˆๅฎŒๆ•ดๅˆ—่กจ
ๅญ˜ๅ‚จSQLite + ORM่ฝป้‡ใ€ไบ‹ๅŠกๆ”ฏๆŒใ€ๆ˜“ไบŽๆŸฅ่ฏข
ไฝœ็”จๅŸŸSession ็บงๅˆซ่‡ช็„ถ้š”็ฆป๏ผŒๅคšไผš่ฏไธๅ†ฒ็ช
ๅŒๆญฅๆœบๅˆถBus ไบ‹ไปถๅ‘ๅธƒๅฎžๆ—ถ UI ๆ›ดๆ–ฐ๏ผŒๆพ่€ฆๅˆ

8.2 ๆƒ้™ๆŽงๅˆถ

opencode ไธญ Todo ๅทฅๅ…ทไฝฟ็”จๆ ‡ๅ‡†ๆƒ้™็ณป็ปŸ๏ผŒSubagent ็ฆๆญขไฝฟ็”จ๏ผš

// general/explore subagent ้…็ฝฎ
{
  name: "general",
  permission: PermissionNext.merge(
    defaults,
    PermissionNext.fromConfig({
      todoread: "deny",   // ็ฆๆญข่ฏปๅ– todo
      todowrite: "deny",  // ็ฆๆญขไฟฎๆ”น todo
    }),
  ),
}

ไธบไป€ไนˆ้™ๅˆถ Subagent๏ผŸ

  • Subagent๏ผˆๅฆ‚ generalใ€explore๏ผ‰ๅชๆ‰ง่กŒๅ•ไธ€ไปปๅŠก
  • ไธๅบ”่ฎฟ้—ฎๆˆ–ไฟฎๆ”น็ˆถไผš่ฏ็š„ todo ๅˆ—่กจ
  • ไฟๆŒๅ…ณๆณจ็‚นๅˆ†็ฆป

8.3 AI ไฝฟ็”จๅœบๆ™ฏ

ๅœบๆ™ฏ่งฆๅ‘ๆกไปถAI ๆ“ไฝœ
ๅˆ›ๅปบๅคšๆญฅ้ชคไปปๅŠก๏ผˆ3+ ๆญฅ้ชค๏ผ‰่ฐƒ็”จ todowrite ๅˆ›ๅปบๅˆๅง‹ๅˆ—่กจ
ๆ›ดๆ–ฐๅผ€ๅง‹ๆ–ฐไปปๅŠกๆ ‡่ฎฐไธบ in_progress
ๅฎŒๆˆไปปๅŠกๅฎŒๆˆๆ ‡่ฎฐไธบ completed
ๆทปๅŠ ๅ‘็Žฐๆ–ฐไปปๅŠก่ฏปๅ–ๅฝ“ๅ‰ๅˆ—่กจ๏ผŒ่ฟฝๅŠ ๆ–ฐไปปๅŠก
ๆฃ€ๆŸฅๆฏ้š” 3-5 ่ฝฎๅฏน่ฏ่ฐƒ็”จ todoread ็กฎ่ฎค่ฟ›ๅบฆ

8.4 ๅฎž็Žฐๅปบ่ฎฎ๏ผˆdm_cc๏ผ‰

# ============ ๅญ˜ๅ‚จๅฑ‚ ============
class TodoStore:
    """Session ็บงๅˆซ็š„ Todo ๅญ˜ๅ‚จ"""
 
    def __init__(self, session_id: str):
        self.session_id = session_id
        self.file_path = TODOS_DIR / f"{session_id}.json"
 
    def get_all(self) -> list[TodoItem]:
        if not self.file_path.exists():
            return []
        data = json.loads(self.file_path.read_text())
        return [TodoItem(**item) for item in data]
 
    def update(self, todos: list[TodoItem]):
        """ๅ…จ้‡ๆ›ฟๆขๆ›ดๆ–ฐ"""
        data = [asdict(todo) for todo in todos]
        self.file_path.write_text(json.dumps(data, indent=2))
 
 
# ============ ๅทฅๅ…ทๅฑ‚ ============
class TodoReadTool(Tool):
    name = "todo_read"
    parameters = None  # ๆ— ๅ‚ๆ•ฐ
 
    async def execute(self, params) -> dict:
        session_id = get_current_session_id()
        store = TodoStore(session_id)
        todos = store.get_all()
        return {
            "title": f"{len([t for t in todos if t.status != 'completed'])} todos",
            "output": format_todos(todos),
            "metadata": {"todos": todos},
        }
 
 
class TodoWriteTool(Tool):
    name = "todo_write"
 
    class Parameters(BaseModel):
        todos: list[TodoItem]
 
    async def execute(self, params: Parameters) -> dict:
        session_id = get_current_session_id()
        store = TodoStore(session_id)
        store.update(params.todos)
        return {"title": "Updated", "output": "Todo list updated"}

้™„ๅฝ•๏ผšๅฎŒๆ•ดๆ–‡ไปถๆธ…ๅ•

ๆ–‡ไปถ่ฏดๆ˜Ž
packages/opencode/src/session/todo.tsๆ•ฐๆฎๆจกๅž‹ๅ’Œๅญ˜ๅ‚จๆ“ไฝœ
packages/opencode/src/session/session.sql.tsๆ•ฐๆฎๅบ“่กจๅฎšไน‰
packages/opencode/src/tool/todo.tsๅทฅๅ…ทๅฎšไน‰
packages/opencode/src/tool/todoread.txt่ฏปๅ–ๅทฅๅ…ทๆ่ฟฐ
packages/opencode/src/tool/todowrite.txtๅ†™ๅ…ฅๅทฅๅ…ทๆ่ฟฐ๏ผˆๅซ่งฆๅ‘ๆกไปถ๏ผ‰
packages/opencode/src/agent/agent.tsAgent ๆƒ้™้…็ฝฎ๏ผˆtodo ็ฆ็”จ่ง„ๅˆ™๏ผ‰
packages/opencode/src/cli/cmd/tui/component/todo-item.tsxUI ็ป„ไปถ
packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsxไพง่พนๆ ๅฑ•็คบ