Make ticket pages open in T3 Code's file viewer, and show plans, mocks and progress inline in the thread
This commit is contained in:
1 parent
e21a4c24f0
commit
4e39be860c
9 files changed
+235
-42
No files matched your search
@@ -1,11 +1,11 @@
|
||||
// ticket-page: the only writer of a ticket's page data. See ../SITE.md for when the
|
||||
// ticket skill calls each command. Run `ticket-page help` for the command list.
|
||||
|
||||
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { htmlToText, importIssue, type JiraIssue, scanResources } from "./jira-import";
|
||||
import { renderView, VIEWS } from "./thread-view";
|
||||
import {
|
||||
type Criterion, type Epic, type Finding, type Mock, now, type Plan, progress, readData, type Step,
|
||||
storyStatus, type Ticket, writeData,
|
||||
@@ -49,24 +49,19 @@ function ticketDir(key: string): string {
|
||||
const ticketFile = (dir: string) => join(dir, "ticket.data.js");
|
||||
const epicFile = (epicKey: string) => join(docsRoot(), epicKey, "epic.data.js");
|
||||
|
||||
/** Copies the shared renderer into _site/ when the skill's copy changed, and writes the
|
||||
* page shell. Run on every write so an old ticket picks up the current renderer. */
|
||||
/** Writes the page shell with the renderer's CSS and JS inlined, so the page carries
|
||||
* everything but its own data file. T3 Code's file viewer serves an HTML file's assets only
|
||||
* from its own folder and below, so a shell that pointed at a shared ../../_site/ renderer
|
||||
* rendered blank there. Run on every write so an old ticket picks up the current renderer. */
|
||||
function refreshSite(pageDir: string, kind: "ticket" | "epic", title: string) {
|
||||
const site = join(docsRoot(), "_site");
|
||||
mkdirSync(site, { recursive: true });
|
||||
const version = createHash("sha1");
|
||||
for (const f of ["site.css", "site.js"]) {
|
||||
const from = join(SITE_TEMPLATES, f), to = join(site, f);
|
||||
const body = readFileSync(from, "utf8");
|
||||
version.update(body);
|
||||
if (!existsSync(to) || readFileSync(to, "utf8") !== body) copyFileSync(from, to);
|
||||
}
|
||||
// The shell names the renderer by content hash, so a browser that cached an older
|
||||
// renderer loads the new one the next time the page opens.
|
||||
const shell = readFileSync(join(SITE_TEMPLATES, `${kind}.html`), "utf8")
|
||||
.replaceAll("{{SITE}}", relative(pageDir, site) || ".")
|
||||
.replaceAll("{{VERSION}}", version.digest("hex").slice(0, 10))
|
||||
.replaceAll("{{TITLE}}", title.replace(/[<&]/g, c => (c === "<" ? "<" : "&")));
|
||||
const fill: Record<string, string> = {
|
||||
CSS: readFileSync(join(SITE_TEMPLATES, "site.css"), "utf8"),
|
||||
JS: readFileSync(join(SITE_TEMPLATES, "site.js"), "utf8"),
|
||||
TITLE: title.replace(/[<&]/g, c => (c === "<" ? "<" : "&")),
|
||||
};
|
||||
// A replacer function, because the renderer's own source is full of `$` sequences that
|
||||
// a replacement string would read as patterns.
|
||||
const shell = readFileSync(join(SITE_TEMPLATES, `${kind}.html`), "utf8").replace(/\{\{(CSS|JS|TITLE)\}\}/g, (_, k: string) => fill[k]);
|
||||
const page = join(pageDir, "index.html");
|
||||
if (!existsSync(page) || readFileSync(page, "utf8") !== shell) writeFileSync(page, shell);
|
||||
}
|
||||
@@ -342,8 +337,24 @@ command("show", "show <KEY> print where the ticket stands", a => {
|
||||
|
||||
command("path", "path <KEY> print the page path", a => console.log(join(ticketDir(a.pos[0]), "index.html")));
|
||||
|
||||
command("open", "open <KEY> open the page in the browser", a => {
|
||||
spawnSync("xdg-open", [join(ticketDir(a.pos[0]), "index.html")], { stdio: "ignore" });
|
||||
command("open", "open <KEY> print the page path, and open it in the browser outside T3 Code", a => {
|
||||
const { dir, t } = load(a.pos[0]);
|
||||
// Rewrites the shells first, so a page last written before the renderer was inlined
|
||||
// opens in T3 too.
|
||||
refreshSite(dir, "ticket", `${t.key}: ${t.title}`);
|
||||
const epic = t.epic && epicFile(t.epic.key);
|
||||
if (epic && existsSync(epic)) { const e = readData(epic, "epic"); refreshSite(dirname(epic), "epic", `${e.key}: ${e.title}`); }
|
||||
const page = join(dir, "index.html");
|
||||
// Inside a T3 Code thread the agent links this path and the page opens in T3's file
|
||||
// viewer, beside the conversation. A browser window would be one more thing to juggle.
|
||||
if (!process.env.T3CODE_THREAD_ID) spawnSync("xdg-open", [page], { stdio: "ignore" });
|
||||
console.log(page);
|
||||
});
|
||||
|
||||
command("view", `view <KEY> <${VIEWS.join("|")}> [--mock <id>] print a thread view for T3 Code's html_render`, a => {
|
||||
const [key, name] = a.pos;
|
||||
const view = VIEWS.find(v => v === name) ?? fail(`view takes ${VIEWS.join(", ")}`);
|
||||
console.log(renderView(load(key).t, view, a.one("mock")));
|
||||
});
|
||||
|
||||
command("md", "md <KEY> print the ticket and plan as markdown, for an agent's reading order", a => {
|
||||
|
||||
Reference in new issue
Block a user