fix: v2.6.1 — panes start in their folder again after an update (#69), Changes panel stacks every repo, clickable paths in chat
- #69: tmux applies a pane's `-c` only while its server's own cwd exists. The server was forked from the dashboard's cwd (the plugin cache dir), outlives updates, and the old cache dir is pruned — after that every new pane started in a deleted directory, where `claude` died at once ("run not found") and a shell printed getcwd errors. tmux calls now run from `/`, and the pane's sh wrapper cds into its folder itself, which also heals a tmux server that is already stuck. - Changes panel: a workspace's extra folders are sections stacked under the workspace folder (VS Code Source Control style), with Close repository / Close other repositories on a header's right-click. - Chat: URLs and absolute paths are links; Claude Code's `[Image: source:]` line folds into its turn; uploads under `<data dir>/drops/` are listed. - Update check: a checkout holding commits a newer release lacks reports `diverged_from_release` instead of offering a fast-forward that can only fail.
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
* subset of CommonMark + GFM that actually appears in Claude Code transcripts:
|
||||
* fenced code blocks, ATX headings, ordered/unordered lists, task lists, blockquotes,
|
||||
* horizontal rules, simple tables, inline code, bold, italic, strikethrough, links,
|
||||
* and auto-linked URLs.
|
||||
* and auto-linked URLs. An absolute file path (bare or in inline code) opens
|
||||
* through `OpenFileContext` when the page provides one; `linkify` does the
|
||||
* same links for plain text.
|
||||
*
|
||||
* Output is built as a React element tree (no dangerouslySetInnerHTML) so user content
|
||||
* is escaped by React. Code blocks delegate to <CodeBlock /> for syntax highlighting and
|
||||
@@ -244,6 +246,71 @@ function parseBlocks(src: string): Block[] {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/** Opens a file a message names; provided by a view that can show one. */
|
||||
export const OpenFileContext = React.createContext<((path: string) => void) | undefined>(undefined);
|
||||
|
||||
const LINK_CLASS =
|
||||
"text-violet-700 dark:text-violet-300 hover:text-violet-800 dark:hover:text-violet-200 underline underline-offset-2 decoration-violet-400/40 hover:decoration-violet-300/70 break-all";
|
||||
const URL_RE = /^https?:\/\/[^\s<>()]+[^\s<>().,!?;:'"]/;
|
||||
// Two segments at least, so "/" or "/s" stays text; a trailing full stop is prose.
|
||||
const PATH_RE = /^(?:\/[^\s/<>()'"`,;:]*[^\s/<>()'"`,;:.!?]){2,}/;
|
||||
// A link starts a word: "and/or" or "a/b/c" stays text.
|
||||
const atWordStart = (text: string, i: number) => i === 0 || /[\s([{"'`]/.test(text[i - 1]!);
|
||||
|
||||
function UrlLink({ url }: { url: string }) {
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer" className={LINK_CLASS}>
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
/** A path as a button that opens it, or as it was when nothing can. */
|
||||
function PathLink({ path, children }: { path: string; children: React.ReactNode }) {
|
||||
const open = React.useContext(OpenFileContext);
|
||||
if (!open) return <>{children}</>;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
title={path}
|
||||
onClick={() => open(path)}
|
||||
className={`${LINK_CLASS} text-left`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/** Plain text with its URLs and absolute paths as links — nothing else parsed. */
|
||||
export function linkify(text: string): React.ReactNode[] {
|
||||
const out: React.ReactNode[] = [];
|
||||
let buf = "";
|
||||
for (let i = 0; i < text.length;) {
|
||||
const rest = text.slice(i);
|
||||
const url = atWordStart(text, i) && URL_RE.exec(rest)?.[0];
|
||||
const path = !url && atWordStart(text, i) && PATH_RE.exec(rest)?.[0];
|
||||
const hit = url || path;
|
||||
if (!hit) {
|
||||
buf += text[i++];
|
||||
continue;
|
||||
}
|
||||
if (buf) out.push(buf);
|
||||
buf = "";
|
||||
out.push(
|
||||
url ? (
|
||||
<UrlLink key={i} url={url} />
|
||||
) : (
|
||||
<PathLink key={i} path={hit}>
|
||||
{hit}
|
||||
</PathLink>
|
||||
)
|
||||
);
|
||||
i += hit.length;
|
||||
}
|
||||
if (buf) out.push(buf);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Render inline markdown (bold/italic/code/strikethrough/links/auto-links). */
|
||||
function renderInline(text: string, baseKey = ""): React.ReactNode[] {
|
||||
const out: React.ReactNode[] = [];
|
||||
@@ -267,11 +334,18 @@ function renderInline(text: string, baseKey = ""): React.ReactNode[] {
|
||||
// Inline code: `...`
|
||||
const codeM = rest.match(/^`([^`\n]+)`/);
|
||||
if (codeM) {
|
||||
push(
|
||||
const code = (
|
||||
<code className="rounded bg-surface-4 border border-surface-3 px-1.5 py-0.5 font-mono text-[12.5px] text-status-warning">
|
||||
{codeM[1]}
|
||||
</code>
|
||||
);
|
||||
push(
|
||||
PATH_RE.exec(codeM[1]!)?.[0] === codeM[1] ? (
|
||||
<PathLink path={codeM[1]!}>{code}</PathLink>
|
||||
) : (
|
||||
code
|
||||
)
|
||||
);
|
||||
i += codeM[0].length;
|
||||
continue;
|
||||
}
|
||||
@@ -330,22 +404,21 @@ function renderInline(text: string, baseKey = ""): React.ReactNode[] {
|
||||
}
|
||||
|
||||
// Auto-link
|
||||
const urlM = rest.match(/^https?:\/\/[^\s<>()]+[^\s<>().,!?;:'"]/);
|
||||
const urlM = rest.match(URL_RE);
|
||||
if (urlM) {
|
||||
push(
|
||||
<a
|
||||
href={urlM[0]}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-violet-700 dark:text-violet-300 hover:text-violet-800 dark:hover:text-violet-200 underline underline-offset-2 decoration-violet-400/40 hover:decoration-violet-300/70 break-all"
|
||||
>
|
||||
{urlM[0]}
|
||||
</a>
|
||||
);
|
||||
push(<UrlLink url={urlM[0]} />);
|
||||
i += urlM[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// An absolute path at a word start
|
||||
const pathM = atWordStart(text, i) ? PATH_RE.exec(rest) : null;
|
||||
if (pathM) {
|
||||
push(<PathLink path={pathM[0]}>{pathM[0]}</PathLink>);
|
||||
i += pathM[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
buf += text[i]!;
|
||||
i++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user