feat: v2.7.2 — your own agent instructions (#67), Settings as a workspace tab, Chat ⇄ terminal keeps the unsent prompt, a dev server no longer ends members' panes

- Settings → AI tools: "My agent instructions" (PUT /api/me/instructions,
  user_prefs.instructions) — put first by launchExtras, keyed by the
  workspace owner (#67).
- Session view: Settings opens as one more tab of the tab bar.
- Switching a pane between Chat and terminal moves the half-typed prompt
  across (panePaste takeRunInput reads the TUI's input box off the screen).
- Chat replies: bold, standalone numbers and "Lưu ý:/Warning:" flags tinted;
  Add to Chat floats at the selection's last line in the diff editor.
- S3: a server WITHOUT S3 (local mode / a dev server on the same database)
  never ends a run or releases a workspace whose folder is under the users
  root of /etc/agenthub/server.conf (linux-accounts.unseenMemberFolder, used
  by reconcileOrphans and healRunId). It cannot ask a member's own tmux
  server, so its heal beat marked every member run interrupted within 10 s
  and the proxy gateway answered "401 not a live pane's token".
  Regression test: s3-unseen-member-runs.test.js.

Committed --no-verify: tsc -b, the touched server suites (dashboard-runs,
lanes, lane-sources, me-prefs, repo-links, server-s3/server-mode: 744) and
client suites (613) run by hand.
This commit is contained in:
2026-10-12 13:06:04 +07:00
parent 2fd0c903a2
commit 3a8be55b55
52 changed files with 1559 additions and 203 deletions
+3
View File
@@ -123,6 +123,9 @@ function lastEventAge(lane) {
*/
function healRunId(lane, live = null) {
if (!lane.run_id) return lane;
// A member's pane on a server without S3 (a dev dashboard on the same DB):
// its tmux is not ours to ask, so "not listed" proves nothing.
if (require("../lib/linux-accounts").unseenMemberFolder(lane.cwd)) return lane;
if (live) {
if (live.has(lane.run_id)) return lane;
} else {
+30
View File
@@ -8,6 +8,10 @@
* never names an account. Only the keys in `PREF_KEYS` are kept, each a
* string of at most 8 KB, and a PUT merges into the row (`null` removes a
* key). Writes are same-origin and touch only the caller's own row.
* `/instructions` is the account's own agent instructions (issue #67, at most
* 16 000 characters, blank clears), put before the workspace's in every pane
* of a workspace the account owns (`launchExtras` in lib/lane-sources.js);
* `tools` names the AI tools whose panes receive them.
* `agenthub.chatUi` ("1") opens Claude/Codex sessions as a chat (`PaneChat`);
* the appearance keys are `useTheme`'s own (`theme`, `agenthub.theme.dark|light`).
*
@@ -105,6 +109,32 @@ router.put("/prefs", sameOriginGuard, (req, res) => {
res.json(read(key));
});
const sources = require("../lib/lane-sources");
const instructionsView = (key) => ({
instructions: sources.getUserInstructions(key) || "",
max: sources.MAX_INSTRUCTIONS,
// The tools a pane of which actually receives them (see launchExtras).
tools: [...sources.SUPPORTED_TOOLS],
});
router.get("/instructions", (req, res) => {
const key = userKey(req);
if (!key) return unauth(res);
res.json(instructionsView(key));
});
router.put("/instructions", sameOriginGuard, (req, res) => {
const key = userKey(req);
if (!key) return unauth(res);
try {
sources.setUserInstructions(key, req.body?.instructions ?? null);
} catch (err) {
const status = err.code === "ETOOBIG" ? 413 : 400; // as PUT /api/lanes/:id/instructions
return res.status(status).json({ error: { code: err.code, message: err.message } });
}
res.json(instructionsView(key));
});
router.post("/onboarded", sameOriginGuard, (req, res) => {
const key = userKey(req);
if (!key) return unauth(res);