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
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# AGENTS.md

## Project

opencode-plugin-loop — `/loop` command plugin for OpenCode (fixed / adaptive / maintenance scheduling).

- Source: `src/` (TypeScript, SolidJS TUI views)
- Tests: `tests/` (node:test, runs against the `dist/` build output)
- Build + test: `npm test` (builds to `dist/` via Babel/tsc, then runs the full suite)

## Development workflow (required)

1. **Branch first**: cut a feature branch (e.g. `feat/xxx`) from the default branch; never commit directly to the default branch.
2. **Verify locally**: `npm test` must pass; for TUI / command-behavior changes, also verify end-to-end in OpenCode.
3. **PR and merge**: open a PR with `gh pr create` and merge into the default branch after verification.
4. **Publish to npm**:
- Bump `package.json` semver after merging (feat → minor, fix → patch).
- The npm access token lives in `/Users/wangshuai/Downloads/npm_access_token.txt`; use the most recently issued token in that file.
- Use the token only via environment variables or throwaway publish-time config (e.g. `//registry.npmjs.org/:_authToken`); **never write it into the repo or commit it**; local `.npmrc` must stay out of git.
- Publish with `npm publish`, then confirm via `npm view opencode-plugin-loop version`.
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# CLAUDE.md

## 项目

opencode-plugin-loop — OpenCode 的 `/loop` 命令插件,支持 fixed / adaptive / maintenance 三种调度模式。

- 源码:`src/`(TypeScript,SolidJS TUI 视图)
- 测试:`tests/`(node:test,运行于 `dist/` 构建产物之上)
- 构建 + 测试:`npm test`(先 Babel/tsc 构建到 `dist/`,再跑全部测试)

## 开发流程(必须遵守)

1. **新分支开发**:所有改动从默认分支切 feature 分支(如 `feat/xxx`),不直接在默认分支上提交。
2. **本地验证**:`npm test` 全部通过;涉及 TUI/命令行为的改动需在 OpenCode 中手动端到端验证。
3. **提交 PR 并合并**:验证通过后 `gh pr create`,合并到默认分支。
4. **发布 npm**:
- 合并后按语义化版本 bump `package.json` 版本号(feat → minor,fix → patch)。
- npm access token 存放在 `/Users/wangshuai/Downloads/npm_access_token.txt`,使用该文件中最新下发的 token 发布。
- token 只通过环境变量或发布时临时配置使用(如 `//registry.npmjs.org/:_authToken`),**绝不写入仓库、绝不提交**;本地 `.npmrc` 不得进入 git。
- 发布:`npm publish`,发布后 `npm view opencode-plugin-loop version` 确认。
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ A drop-in `/loop` command for [opencode](https://opencode.ai), modeled after Cla
- **Auto-expire** — tasks idle for more than 7 days are removed on load (active tasks never expire)
- **Max 50 concurrent tasks**
- **LLM-callable tools** — `loop_schedule`, `loop_status` (session-bound by default)
- **Interactive Loop results** — `/loop` results open in a dedicated native dialog instead of writing over the prompt
- **Interactive task list** — `/loop list` and `/loop status` open a dedicated native dialog with selectable task rows instead of writing over the prompt; start/cancel/pause/resume stay silent, failures surface as error toasts
- **Clipboard actions** — copy the complete result or copy any displayed task ID with one action
- **Keyboard and mouse navigation** — move with `Up`/`Down` or `Tab`/`Shift+Tab`, hover with the pointer, and activate with `Enter`, `Space`, or a click
- **Responsive layout** — short or narrow terminals keep the dialog inside the viewport with independently scrollable result and action areas
- **Responsive layout** — short or narrow terminals keep the dialog inside the viewport with a scrollable task list
- **Easy dismissal** — choose **Close**, press `q`, or use the native dialog's `Esc` key

## Requirements
Expand Down Expand Up @@ -187,15 +187,15 @@ If you try `cancel <id>` for a task owned by another session, you'll get a refus

Two behavioral differences worth knowing: tasks only fire for the **currently active session** (switch sessions and the others wait; switch back and they catch up once), and fixed tasks fire on a 5-second ticker rather than exact wall-clock cron times (up to one ticker period late).

### Interactive result dialog
### Interactive task list dialog

Every `/loop` command result opens in a separate native OpenCode dialog. It keeps task output away from the prompt and provides:
`/loop list` and `/loop status` open a native OpenCode dialog rendering your tasks as a selectable, color-coded list (▶ active, ⏸ paused). Starting, cancelling, pausing, or resuming a task stays silent; failures surface as error toasts. The dialog provides:

- **Copy ID: `<taskId>`** for every distinct task shown in the result
- A highlighted **task row** per task — press `Enter` to copy its task ID
- **Copy all** for the exact complete result text
- **Close** to dismiss the dialog

Use `Up`/`Down` or `Tab`/`Shift+Tab` to change the selected action, then press `Enter` or `Space` to activate it. Moving the mouse over a row selects it, and clicking activates that exact row. A successful **Copy ID** or **Copy all** action shows a confirmation and closes the dialog immediately; if clipboard access fails, the dialog stays open and shows an error. Press `Page Up` or `Page Down` to scroll long result text, or press `q` or `Esc` to close. In short or narrow terminals, the dialog scales to the available viewport and keeps the result and action lists independently scrollable. A newer Loop result replaces the previous Loop dialog rather than stacking another one.
Use `Up`/`Down` or `Tab`/`Shift+Tab` to change the selected row, then press `Enter` or `Space` to activate it. Moving the mouse over a row selects it, and clicking activates that exact row. A successful copy shows a confirmation and closes the dialog immediately; if clipboard access fails, the dialog stays open and shows an error. Press `Page Up` or `Page Down` to scroll long lists, or press `q` or `Esc` to close. In short or narrow terminals, the dialog scales to the available viewport and keeps the list scrollable. A newer Loop result replaces the previous Loop dialog rather than stacking another one.

### Programmatic (LLM tools)

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-plugin-loop",
"version": "0.4.0",
"version": "0.5.0",
"description": "/loop command for opencode — run prompts on a schedule (fixed, adaptive, or maintenance), modeled after Claude Code's /loop",
"type": "module",
"main": "./dist/index.js",
Expand Down
6 changes: 5 additions & 1 deletion src/runtime-feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,17 @@ export async function showLoopResult(
result: CommandParseResult,
logger: LoopLogger
): Promise<void> {
const variant = toastVariant(result.message)
// Non-view results (start/cancel/pause/resume/stop-all) stay silent by design;
// only task lists (info) and failures (error) surface a toast.
if (variant === "success") return
try {
await client.tui.showToast({
throwOnError: true,
body: {
title: LOOP_FEEDBACK_TITLE,
message: result.message,
variant: toastVariant(result.message),
variant,
duration: toastDuration(result.message),
},
})
Expand Down
24 changes: 16 additions & 8 deletions src/tui-dialog-layout.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
export interface LoopDialogRows {
maxHeight: number
messageRows: number
actionRows: number
listRows: number
}

export function allocateLoopDialogRows(
terminalRows: number,
actionCount: number
itemCount: number,
taskList = false
): LoopDialogRows {
const rows = Math.max(1, Math.floor(terminalRows))
const available = Math.max(1, rows - 4)
const maxHeight = Math.min(28, available, Math.max(6, Math.floor(rows * 0.7)))
const contentRows = Math.max(0, maxHeight - 3)
const items = Math.max(0, Math.floor(itemCount))

if (taskList) {
// header (1) + gaps (2) + hint bar (1) + gap (1)
const contentRows = Math.max(1, maxHeight - 5)
return { maxHeight, messageRows: 0, listRows: Math.min(items, contentRows) }
}

const contentRows = Math.max(0, maxHeight - 3)
if (contentRows < 2) {
return { maxHeight, messageRows: 0, actionRows: contentRows }
return { maxHeight, messageRows: 0, listRows: contentRows }
}

const actionRows = Math.min(
Math.max(0, actionCount),
const listRows = Math.min(
items,
Math.max(1, Math.min(6, Math.floor(contentRows * 0.4)))
)

return {
maxHeight,
messageRows: contentRows - actionRows,
actionRows,
messageRows: contentRows - listRows,
listRows,
}
}

Expand Down
Loading
Loading