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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Preserve supplier bytes/CRLF metadata while retaining real whitespace checks.
web/fastui/** -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol
2 changes: 1 addition & 1 deletion .github/workflows/tag-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:

deploy:
needs: validate
if: startsWith(github.ref, 'refs/tags/v')
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
uses: FastR-D/FastDeploy/.github/workflows/deploy.yml@9a2c51dcd63f47a0e6c8f6bb07bd22ea589abbd2
with:
app: fastlabs
Expand Down
9 changes: 6 additions & 3 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import argparse
import json
import math
import mimetypes
import os
import re
Expand Down Expand Up @@ -221,8 +222,8 @@ def parse_agent_timeout(value):
timeout = float(raw)
except (TypeError, ValueError) as exc:
raise ValueError("FASTLAB_AGENT_TIMEOUT 必须是大于等于 0 的秒数。") from exc
if timeout < 0:
raise ValueError("FASTLAB_AGENT_TIMEOUT 必须是大于等于 0 的秒数。")
if not math.isfinite(timeout) or timeout < 0:
raise ValueError("FASTLAB_AGENT_TIMEOUT 必须是有限且大于等于 0 的秒数。")
return timeout


Expand Down Expand Up @@ -2068,6 +2069,8 @@ def start_task(self, task_id):

def _start_task_locked(self, task_id):
task = self.require_task(task_id)
if task["cancel_requested"]:
raise ValueError("任务已请求取消。")
if task["status"] != "awaiting_approval":
raise ValueError("任务尚未准备好或已经开始。")
for subtask in task["subtasks"]:
Expand Down Expand Up @@ -2986,7 +2989,7 @@ def cancel_task(self, task_id):

def _cancel_task_locked(self, task_id):
task = self.require_task(task_id)
if task["status"] not in {"planning", "running", "verifying"}:
if task["status"] not in {"planning", "awaiting_approval", "running", "verifying"}:
raise ValueError("当前任务不在运行。")
self.store.update_task(task_id, cancel_requested=1)
self.agent_gate.wake_all()
Expand Down
32 changes: 32 additions & 0 deletions tests/test_fastlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,35 @@ def test_cancel_stops_running_workers(self):
self.assertTrue(branch_exists(self.repo, "user-keep"))
self.assertFalse((self.data / "worktrees" / task["id"]).exists())

def test_cancel_awaiting_approval_does_not_start_work(self):
task = self.prepare_task()
finish_entered = threading.Event()
release_finish = threading.Event()
original_finish = self.app._finish_cancel

def blocked_finish(snapshot):
finish_entered.set()
self.assertTrue(release_finish.wait(5))
return original_finish(snapshot)

with patch.object(self.app, "_finish_cancel", side_effect=blocked_finish):
returned = self.app.cancel_task(task["id"])
self.assertEqual(returned["status"], "awaiting_approval")
self.assertTrue(returned["cancel_requested"])
self.assertTrue(finish_entered.wait(5))
with self.assertRaises(ValueError):
self.app.start_task(task["id"])
release_finish.set()

worker_calls = [
call for call in self.adapter.calls
if call["key"].startswith("subtask-%s-" % task["id"])
]
self.assertEqual(worker_calls, [])
self.assertFalse((self.data / "worktrees" / task["id"]).exists())
cancelled = self.wait_for_status(task["id"], "cancelled")
self.assertTrue(all(item["status"] == "cancelled" for item in cancelled["subtasks"]))

def test_failure_conflict_and_retry_preserve_main(self):
task = self.prepare_task("CONFLICT")
base = git(self.repo, "rev-parse", "main")
Expand Down Expand Up @@ -1267,6 +1296,9 @@ def test_command_helpers_support_windows(self):
self.assertEqual(parse_agent_timeout("12.5"), 12.5)
with self.assertRaisesRegex(ValueError, "大于等于 0"):
parse_agent_timeout("-1")
for raw in ("nan", "NaN", "inf", "Infinity", "1e9999"):
with self.subTest(raw=raw), self.assertRaisesRegex(ValueError, "有限"):
parse_agent_timeout(raw)

def test_cycle_and_unknown_executor_are_rejected(self):
cycle = {
Expand Down
5 changes: 5 additions & 0 deletions web/FASTUI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# FastUI 0.1.2

部署前运行 `python web/supply_fastui.py`。离线供应:`python web/supply_fastui.py --artifact /path/to/ui-foundation-0.1.2.tgz`。脚本从固定官方 npm tarball URL 下载或读取同字节 tarball,校验 SHA-256 后生成 `web/fastui/`,含版本、包摘要、逐文件摘要和许可证;供应目录不手工编辑。升级时从公开 registry metadata 确认实际 URL,并下载核准版本和完整摘要。

提交/部署生成目录后,服务运行只读取本地文件,不需要 npm/CDN。颜色模式初始化在 HTML 内容之前执行,`fastlabs-theme` 未配置时跟随系统;共享控件通过显式 `fastui-*` 类 opt-in,宿主业务布局使用共享 token。任务表单字段、事件委托和调度状态不变。
10 changes: 5 additions & 5 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,14 @@ function renderCreate() {
main.innerHTML = `
<section class="page-heading"><div><h2 class="page-title">创建任务</h2></div></section>
<form class="panel task-form" id="task-form">
<div class="field"><label>仓库</label><select name="repositoryId" required>${available.map((item) => `<option value="${item.id}" ${item.is_default ? "selected" : ""}>${escapeHtml(item.alias)}</option>`).join("")}</select></div>
<div class="field"><label>仓库</label><select class="fastui-select" name="repositoryId" required>${available.map((item) => `<option value="${item.id}" ${item.is_default ? "selected" : ""}>${escapeHtml(item.alias)}</option>`).join("")}</select></div>
<div class="field"><label>目录 <span class="field-hint">可选</span></label><input name="workingSubdir" placeholder="例如 web"><span class="field-note">仓库内的相对目录</span></div>
<div class="field full"><label>标题 <span class="field-hint">可选</span></label><input name="title" maxlength="80" placeholder="一句话标题"></div>
<div class="field full"><label>目标</label><textarea name="goal" rows="7" required placeholder="要完成什么?"></textarea></div>
<div class="field full"><label>限制 <span class="field-hint">可选</span></label><textarea name="constraints" rows="3" placeholder="不能修改什么?"></textarea></div>
<div class="field full"><label>标题 <span class="field-hint">可选</span></label><input class="fastui-input" name="title" maxlength="80" placeholder="一句话标题"></div>
<div class="field full"><label>目标</label><textarea class="fastui-textarea" name="goal" rows="7" required placeholder="要完成什么?"></textarea></div>
<div class="field full"><label>限制 <span class="field-hint">可选</span></label><textarea class="fastui-textarea" name="constraints" rows="3" placeholder="不能修改什么?"></textarea></div>
<div class="field"><label>并发数</label><input name="maxConcurrency" type="number" min="1" max="${state.settings?.globalConcurrency || 4}" value="${Math.min(4, state.settings?.globalConcurrency || 4)}" required></div>
<div class="field"><label>规划工具</label><input value="${plannerLabel(planner)}" readonly></div>
<div class="form-actions full"><button class="primary-button" type="submit">生成计划</button></div>
<div class="form-actions full"><button class="primary-button fastui-button fastui-button--primary" type="submit">生成计划</button></div>
</form>`;
}

Expand Down
21 changes: 21 additions & 0 deletions web/fastui/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 FastR-D contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
23 changes: 23 additions & 0 deletions web/fastui/NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Source and publication scope

The owner has authorized publication of the generic UI portions under MIT.
Vue semantic primitives and v-model adapters derive from the common
`primitives.ts` (203 lines) in FastPPT and FastFigure. Modal's public props and
the focus-lifecycle purpose derive from their common `use-dialog-focus.ts`
(58 lines) and Modal component. FastUI delegates focus trapping, inertness,
nesting and focus restoration to the browser's native modal dialog rather
than maintaining the original hand-written trap. Select retains the shared
value/options/change contract and delegates interaction to the native select.

The palette follows FastWrite's semantic colors and the approved FastTask
workbench design. Only generic UI code and semantic design values are included.
No private artwork, icons, logos, fonts, application data, product-specific
workflows or brand assets are distributed. Upstream product repositories
retain ownership of their application-specific code and assets.

Source reference revisions: FastPPT `f6bf21f13445c2507719928fa68550552a8fcab2`
(`packages/components/src/`) and FastFigure
`13dcc4179fd0073da76f7626264af8605d5e8c70` (`src/components/ui/`).
At both revisions, `primitives.ts` has Git blob
`c7123af5f8316bc0486bce5c99cfbb2e9ab3dc73` and `use-dialog-focus.ts` has
Git blob `3ffbb6c6c7bd5e3c96f2b8c05c5e008dcdfc4cab`.
75 changes: 75 additions & 0 deletions web/fastui/color-mode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use strict";
(() => {
// packages/foundation/src/index.ts
var parse = (value) => value === "light" || value === "dark" ? value : "system";
function initColorMode({ storageKey: storageKey2, target = document.documentElement }) {
const win = target.ownerDocument.defaultView;
const media = win.matchMedia("(prefers-color-scheme: dark)");
let mode = "system";
try {
mode = parse(win.localStorage.getItem(storageKey2));
} catch {
}
let disposed = false;
const listeners = /* @__PURE__ */ new Set();
const resolved = () => mode === "system" ? media.matches ? "dark" : "light" : mode;
const apply = () => {
target.dataset.fastuiTheme = resolved();
listeners.forEach((listener) => listener({ mode, resolved: resolved() }));
};
const systemChange = () => {
if (mode === "system") apply();
};
const storageChange = (event) => {
let storage;
try {
storage = win.localStorage;
} catch {
return;
}
if (event.storageArea !== storage || event.key !== storageKey2 && event.key !== null) return;
mode = parse(event.newValue);
apply();
};
media.addEventListener("change", systemChange);
win.addEventListener("storage", storageChange);
apply();
return {
get mode() {
return mode;
},
get resolved() {
return resolved();
},
setMode(value) {
if (disposed) return;
mode = parse(value);
try {
win.localStorage.setItem(storageKey2, mode);
} catch {
}
apply();
},
subscribe(listener) {
if (disposed) return () => {
};
listeners.add(listener);
listener({ mode, resolved: resolved() });
return () => {
listeners.delete(listener);
};
},
dispose() {
disposed = true;
media.removeEventListener("change", systemChange);
win.removeEventListener("storage", storageChange);
listeners.clear();
}
};
}

// packages/foundation/src/bootstrap.ts
var storageKey = document.currentScript.dataset.storageKey;
if (!storageKey) throw new Error("FastUI color-mode.js requires data-storage-key on its script element.");
initColorMode({ storageKey });
})();
61 changes: 61 additions & 0 deletions web/fastui/controls.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* Every appearance rule is explicitly opted into by a fastui class. No reset. */
.fastui-button, .fastui-input, .fastui-textarea, .fastui-select {
box-sizing: border-box;
font-family: var(--fastui-font);
font-size: 13px;
line-height: 1.5;
min-height: var(--fastui-height-md);
padding: 7px 10px;
border: 1px solid var(--fastui-border-strong);
border-radius: var(--fastui-radius-control);
background: var(--fastui-surface);
color: var(--fastui-text);
max-width: 100%;
min-width: 0;
color-scheme: var(--fastui-color-scheme);
}
.fastui-button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; font-weight: 500; text-decoration: none; }
.fastui-button--sm, .fastui-input--sm, .fastui-select--sm { min-height: var(--fastui-height-sm); padding: 4px 8px; font-size: 12px; }
.fastui-button--xs, .fastui-input--xs, .fastui-select--xs { min-height: var(--fastui-height-xs); padding: 1px 6px; font-size: 12px; }
.fastui-button:hover:not(:disabled) { background: var(--fastui-selected); }
.fastui-button--primary { background: var(--fastui-primary); border-color: var(--fastui-primary); color: var(--fastui-on-primary); }
.fastui-button--primary:hover:not(:disabled) { background: var(--fastui-primary-hover); }
.fastui-button--danger { background: var(--fastui-danger-fill); border-color: var(--fastui-danger-fill); color: var(--fastui-on-primary); }
.fastui-button--danger:hover:not(:disabled) { filter: brightness(.9); background: var(--fastui-danger-fill); }
.fastui-button--quiet, .fastui-button--icon, .fastui-button--navigation { background: transparent; border-color: transparent; }
.fastui-button--icon { aspect-ratio: 1; padding: 5px; }
.fastui-button--navigation { border-radius: 0; }
.fastui-button[aria-pressed=true], .fastui-button[aria-selected=true] { box-shadow: inset 0 -2px var(--fastui-focus); }
.fastui-button:disabled, .fastui-input:disabled, .fastui-textarea:disabled, .fastui-select:disabled { opacity: .6; cursor: not-allowed; }
.fastui-button:focus-visible, .fastui-input:focus-visible, .fastui-textarea:focus-visible, .fastui-select:focus-visible, .fastui-link:focus-visible, .fastui-disclosure-trigger:focus-visible { outline: 2px solid var(--fastui-focus); outline-offset: 2px; }
.fastui-input::placeholder, .fastui-textarea::placeholder { color: var(--fastui-muted); }
.fastui-input[aria-invalid=true], .fastui-textarea[aria-invalid=true] { border-color: var(--fastui-danger); }
.fastui-input[type=checkbox], .fastui-input[type=radio], .fastui-input[type=range] { min-height: auto; padding: 0; accent-color: var(--fastui-primary); }
.fastui-textarea { resize: vertical; }
.fastui-field { display: grid; gap: 6px; color: var(--fastui-secondary); font: 13px/1.5 var(--fastui-font); }
.fastui-fieldset { border: 1px solid var(--fastui-border); border-radius: var(--fastui-radius-panel); min-width: 0; padding: 16px; }
.fastui-legend { color: var(--fastui-secondary); padding: 0 4px; }
.fastui-heading { color: var(--fastui-text); font-weight: 600; }
.fastui-link { color: var(--fastui-focus); text-underline-offset: 3px; }
.fastui-progress { accent-color: var(--fastui-primary); }
.fastui-table { border-collapse: collapse; text-align: left; }
.fastui-table :where(th, td) { padding: 12px; border-bottom: 1px solid var(--fastui-border); }
.fastui-table th { background: var(--fastui-raised); font-weight: 600; }
.fastui-disclosure-trigger { padding: 5px 8px; cursor: pointer; }
.fastui-dialog { box-sizing: border-box; width: min(620px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow: auto; padding: 0; border: 1px solid var(--fastui-border); border-radius: var(--fastui-radius-panel); background: var(--fastui-surface); color: var(--fastui-text); font: 14px/1.5 var(--fastui-font); box-shadow: 0 16px 48px #0004; }
.fastui-dialog--lg { width: min(880px, calc(100% - 32px)); }
.fastui-dialog--xl { width: min(1100px, calc(100% - 32px)); }
.fastui-dialog::backdrop { background: var(--fastui-overlay); }
.fastui-dialog > :where(header, footer), .fastui-modal-body { padding: 20px 24px; }
.fastui-dialog > header { border-bottom: 1px solid var(--fastui-border); }
.fastui-dialog > footer { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--fastui-border); }
.fastui-modal-body { display: grid; gap: 16px; }
.fastui-alert, .fastui-badge, .fastui-empty, .fastui-loading { font-family: var(--fastui-font); color: var(--fastui-text); }
.fastui-alert { border: 1px solid var(--fastui-border); border-left: 3px solid var(--fastui-tone, var(--fastui-focus)); border-radius: var(--fastui-radius-control); padding: 12px 16px; background: var(--fastui-surface); }
.fastui-badge { display: inline-block; border: 1px solid currentColor; color: var(--fastui-tone, var(--fastui-focus)); border-radius: 4px; padding: 1px 6px; font-size: 12px; }
.fastui-tone-success { --fastui-tone: var(--fastui-success); }
.fastui-tone-warning { --fastui-tone: var(--fastui-warning); }
.fastui-tone-danger { --fastui-tone: var(--fastui-danger); }
.fastui-empty { padding: 24px; text-align: center; }
.fastui-empty > h2 { font-size: 16px; }
.fastui-loading { display: inline-flex; align-items: center; gap: 8px; }
12 changes: 12 additions & 0 deletions web/fastui/provenance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"package": "@fastrd/ui-foundation",
"version": "0.1.2",
"url": "https://registry.npmjs.org/@fastrd/ui-foundation/-/ui-foundation-0.1.2.tgz",
"sha256": "c3dabbad16980242f66db07619b161889d0ee2492daaa4aaddee1583bc7bcd0b",
"files": {
"tokens.css": "fadf2cf99aa54150ff65776032c4f61b007ebe47a0051723f81afac0b397de38",
"controls.css": "f34fa4d48fbae9dcbf22fc7fcb1718e2b1a5cb33c36bc287805fd49b61cecbc4",
"color-mode.js": "596e123cf27bf280dec4cabafe98dfbcf780fbef546763c8c27a0c28e6c52f99"
},
"generated_by": "python web/supply_fastui.py"
}
Loading
Loading