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: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
"name": "cc-dazzle",
"source": "./plugins/cc-dazzle",
"description": "Draws Claude Code's transcript in Catppuccin Mocha: boxed code blocks, box tables, Mermaid diagrams, diff cards in Claude's colors, compact tool rows and timed prompts. Optional stretchtext replies. Claude Code only.",
"version": "1.7.1"
"version": "1.8.0"
}
]
}
24 changes: 21 additions & 3 deletions docs/upstreams.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,34 @@ Plugins here that copy code from another project, and the last upstream version

| Plugin | Upstream | Copied from | Last checked | Checked on |
|---|---|---|---|---|
| cc-dazzle | [NahumLitvin/prismantis](https://github.com/NahumLitvin/prismantis) | fork `vdsmon/prismantis` 0.15.19, which split from upstream at 0.15.1 (`af067b6`) | 0.31.0 (`354e11d`) | 2026-10-19 |
| cc-dazzle (stretchtext) | [pro-vi/cc-stretch](https://github.com/pro-vi/cc-stretch) | 0.3.3 (`4466e11`) | 0.5.1 (`9aa56da`) | 2026-10-19 |
| cc-dazzle | [NahumLitvin/prismantis](https://github.com/NahumLitvin/prismantis) | fork `vdsmon/prismantis` 0.15.19, which split from upstream at 0.15.1 (`af067b6`) | 0.32.1 (`b90c326`) | 2026-10-20 |
| cc-dazzle (stretchtext) | [pro-vi/cc-stretch](https://github.com/pro-vi/cc-stretch) | 0.3.3 (`4466e11`) | 0.5.2 (`1de5f05`) | 2026-10-20 |

To list what is new since the last check:

```console
$ git clone https://github.com/NahumLitvin/prismantis && cd prismantis
$ git log --oneline 354e11d..HEAD
$ git log --oneline b90c326..HEAD
```

## prismantis 0.31.0 to 0.32.1

Ported: a line holding only an HTML-like tag (`<tldr>`) drawn dim on its own (#228); gantt bars that start on a half cell and end on an eighth (#224); from the diff-card work (#234), the no-newline marker as its own row, a dim `...` between hunks, each hunk highlighted apart with removed lines plain, and a node allowance that keeps an opened card under Claude Code's 20000-node ceiling; new, deleted, emptied and renamed files drawn as `Created`, `Deleted` and `Renamed` cards, an Edit that makes a file and a Write preview included, and a write held for review left to Claude Code (#236); clickable paths in replies, tool rows and edit card headers, with a `file://` link kept only when its label shows its own path and programs and launchers never linked (#238, #240); and the redraw when the html copy check lands (#238). While porting the gantt bars, cc-dazzle's gantt charts turned out never to fit under about 90 columns, because the box frame takes 4 more columns than upstream's; `mermaid.tsx` now sizes them for it.

Left out on purpose:

- A click anywhere on a folded card opens it (#236): it needs each card redrawn as one padded Text inside a Button. The `+N more lines` and `show N lines` buttons do the same. A good next port.
- A wrapped diff line repeating its mark on each row (#234): it needs the surface width split into rows by hand; lines wrap under their code as before.
- Diff colors from the theme, the darker light-theme greens, `diffStyle` and the preview hover band (#234, #236): cc-dazzle draws diffs in Claude Code's own colors, Mocha only.
- The WSL `file://wsl.localhost` rewrite (#238): cc-dazzle dropped Linux and Windows clipboard paths. Paths in stretchtext replies do not link yet: `stretch/layout.ts` draws them.
- Figures (#171, #243, #248), including figure role markup outside figures: cc-dazzle has no figure prompt, so Claude never writes that markup.

## cc-stretch 0.5.1 to 0.5.2

Ported: the words before a lone period, or any stretch with no words of its own, open its stretch; links act when the button comes up, so a drag from a link selects; a double-click on lead-in words still copies the word; the note to Claude names a wordless stretch by its lead-in words; a code span or markdown link never pairs across a pressable phrase's marks. `parse.ts`, `grow.ts` and `guide.ts` stay upstream's, unchanged.

Left out on purpose: the bench and film scripts and docs, which never ship.

## prismantis 0.24.0 to 0.31.0

Ported: hover copy buttons that cover only their own cells (#170), the 8-bit control character filter with the escape-sequence filter it extends (#188, #98), code drawn through the app's own Code element off the terminal (#176), Lua highlighting (#216), one color and a legend name per xychart series (#225), and pie and gantt charts (#177, #221), with the pie always round where it fits (upstream's `pieStyle` default). Both vendor files and `hooks/charts.ts` are upstream's, unchanged.
Expand Down
2 changes: 1 addition & 1 deletion plugins/cc-dazzle/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cc-dazzle",
"version": "1.7.1",
"version": "1.8.0",
"description": "Draws Claude Code's transcript in Catppuccin Mocha: boxed code blocks, box tables, Mermaid diagrams, diff cards in Claude's colors, compact tool rows and timed prompts. Optional stretchtext replies.",
"author": {
"name": "Victor De Simone",
Expand Down
8 changes: 5 additions & 3 deletions plugins/cc-dazzle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ Mods that draw the same row do not stack, so turn off any other mod that draws r

- **Replies:** a blank line before each part, headings in lavender with a heavy rule under H1, boxed tables with a double line under the header, `[ ]` and `[✓]` task lists, and version numbers and file paths in color. Each color has one job: mauve marks who is talking, red marks only failures.
- **Code:** a rounded box with the language and a copy button above it, Prism colors for 25 languages, and `console` blocks with the `$` lines colored as commands. Their copy button copies the commands only. In the desktop app, VS Code and the phone app, code goes through the app's own code element, so indentation holds; `console` blocks there take the app's colors.
- **Mermaid:** flowcharts, charts and other diagrams drawn as box art. Each series of a chart gets its own color, and the legend shows its name. A pie chart is a round circle with a legend (one bar per slice past seven slices or in a narrow terminal), and a gantt chart puts its tasks on a shared time axis. A diagram that does not fit shows its source with a note.
- **Mermaid:** flowcharts, charts and other diagrams drawn as box art. Each series of a chart gets its own color, and the legend shows its name. A pie chart is a round circle with a legend (one bar per slice past seven slices or in a narrow terminal), and a gantt chart puts its tasks on a shared time axis, its bars ending on an eighth of a cell. A diagram that does not fit shows its source with a note.
- **Copy buttons:** `⧉ copy` on code, lists and quotes. Tables get `⧉ md`, `⧉ art` and, on macOS outside SSH, `⧉ html`, which pastes as a real table in Docs, Notion or email.
- **Tool calls:** one dim row each (`Ran gh pr view 12`, `Read ~/src/app.ts`) under a bold `└─`. Bash and MCP output in columns shows as a boxed table with a row count and the run time.
- **Edits:** a diff card in Claude Code's colors, with the changed words marked, code colored by file type, and long diffs folded.
- **Edits:** a diff card in Claude Code's colors, with the changed words marked, added and unchanged lines colored by file type, a dim `...` between hunks, and long diffs folded. A new file shows `Created <path> (N lines)` over its first 10 lines, a file a shell command deletes is one `Deleted <path> (N lines)` line that opens to its lines, and a file moved unchanged is one `Renamed <old> → <new>` line.
- **Paths:** in the terminal, from Claude Code 2.1.289, file paths in replies, tool rows and edit cards open on click. A `file://` link opens only when its label shows the path it opens, and programs and launchers never open.
- **Your prompts:** a rounded box with the send time (`14:32`, or `Oct 10 14:32` for another day). A prompt you send while Claude works stays gray until Claude reads it.
- **Slash-command output:** drawn like a reply, unless it has terminal color codes.

Expand All @@ -34,6 +35,7 @@ Off by default. Turn it on in `/config` (cc-dazzle, **Stretchtext replies**); th

When it is on, Claude writes each reply short, with `{{short|long}}` stretches. A stretch draws as its short form, underlined. Click it and the long form grows in place around the words you already read, with a grey `▎` in the left margin beside the lines that hold the new words; click `«` to shrink it back. Stretches can nest. With your next prompt, Claude is told which stretches you opened.

- A stretch with no words of its own, such as a lone period, opens from the words before it.
- Clicks work in the fullscreen terminal (`/tui fullscreen`) and in the desktop app. Elsewhere every stretch shows open, so nothing is hidden.
- From the keyboard, in the fullscreen terminal: alt+↓ or ctrl+↓ puts a red letter on each closed stretch in the last three replies on screen; type the letter to open it. alt+↑ or ctrl+↑ does the same for the `«` closers. Any other key puts the letters away. Clicking the reply's `●` does what alt+↓ does.
- In a paragraph with a stretch, drag to select text or double-click a word, path or URL; the selection goes to the clipboard. Links and bare URLs there are clickable.
Expand All @@ -49,4 +51,4 @@ When it is on, Claude writes each reply short, with `{{short|long}}` stretches.

## Credits

Most of the code comes from [prismantis](https://github.com/NahumLitvin/prismantis) by Nahum Litvin, through the fork `vdsmon/prismantis` at 0.15.19. Upstream prismantis stays a source of ideas: they are copied here by hand. The stretchtext code comes from [cc-stretch](https://github.com/pro-vi/cc-stretch) 0.3.3 by pro-vi, with fixes and features from releases up to 0.5.1, after Ted Nelson's stretchtext. `hooks/vendor/` holds frozen builds of [Prism](https://github.com/PrismJS/prism) 1.30.0 and [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid) 1.1.3. See `NOTICE` for the licenses.
Most of the code comes from [prismantis](https://github.com/NahumLitvin/prismantis) by Nahum Litvin, through the fork `vdsmon/prismantis` at 0.15.19. Upstream prismantis stays a source of ideas: they are copied here by hand. The stretchtext code comes from [cc-stretch](https://github.com/pro-vi/cc-stretch) 0.3.3 by pro-vi, with fixes and features from releases up to 0.5.2, after Ted Nelson's stretchtext. `hooks/vendor/` holds frozen builds of [Prism](https://github.com/PrismJS/prism) 1.30.0 and [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid) 1.1.3. See `NOTICE` for the licenses.
44 changes: 4 additions & 40 deletions plugins/cc-dazzle/hooks/blocks.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ElementTable, RenderElement } from 'claude-code'

import { tableHtml, tableText } from './html'
import { drawable } from './links'
import type { Block, Inline } from './markdown'
import { displayText, inlineText } from './markdown'
import { graphemes, hasLink, longestWord, mostLines, remember, sliceInline, width, wrapInline, wrapRanges } from './text'
Expand All @@ -9,43 +10,6 @@ import { ALERT_COLOR } from './theme'
import type { PrismToken } from './vendor/prism.js'
import { languages, tokenize } from './vendor/prism.js'

const isHyperlink = (href: string, http: boolean, editor: boolean): boolean =>
(http ? /^https?:/i : /^(https:|http:\/\/localhost(:\d+)?([/?#]|$))/i).test(href) || (editor && /^vscode(-insiders)?:\/\/file\//i.test(href))

const plainLink = (text: string, href: string): string => {
const target = href.replace(/^mailto:/i, '')
return text === target || `http://${text}` === target ? target : `${text} (${target})`
}

const unlink = (nodes: Inline[], http: boolean, editor: boolean): Inline[] =>
nodes.map(n => (n.kind === 'link' && !isHyperlink(n.href, http, editor) ? { kind: 'text', text: plainLink(n.text, n.href) } : 'children' in n ? { ...n, children: unlink(n.children, http, editor) } : n))

const unlinkBlock = (block: Block, http: boolean, editor: boolean): Block => {
const plain = (nodes: Inline[]) => unlink(nodes, http, editor)
switch (block.kind) {
case 'heading':
case 'paragraph':
return { ...block, inline: plain(block.inline) }
case 'list':
return { ...block, items: block.items.map(item => ({ ...item, inline: plain(item.inline) })) }
case 'table':
return { ...block, header: block.header.map(plain), rows: block.rows.map(r => r.map(plain)) }
case 'notes':
return { ...block, notes: block.notes.map(note => ({ ...note, inline: plain(note.inline) })) }
default:
return block
}
}

const unlinked = new WeakMap<Block, Block[]>()

// A link Claude Code cannot open draws as its text and target, so nothing looks clickable that is not.
const drawable = (block: Block, http: boolean, editor: boolean): Block => {
const cached = unlinked.get(block) ?? []
unlinked.set(block, cached)
return (cached[+http + 2 * +editor] ??= unlinkBlock(block, http, editor))
}

const noteHover = (style: Style, note: number | undefined) =>
note === undefined || style.noteKey === undefined ? {} : { hover: { scope: `fn:${style.noteKey}:${note}`, inverse: true } }

Expand All @@ -72,7 +36,7 @@ const renderInline = (el: ElementTable, style: Style, nodes: Inline[], keyBase:
case 'number':
return <Text key={key} color={t.number}>{n.text}</Text>
case 'path':
return <Text key={key} color={t.path}>{n.text}</Text>
return <Text key={key} color={t.path}>{n.href === undefined ? n.text : <el.Link href={n.href} label={n.text} />}</Text>
case 'dim':
return <Text key={key} dimColor>{n.text}</Text>
case 'footnote':
Expand Down Expand Up @@ -496,7 +460,7 @@ const renderList = (el: ElementTable, style: Style, block: Extract<Block, { kind

export type CopyButton = (text: string | (() => string), key: string, label?: string, html?: () => string) => RenderElement | null
export type Drawn = Map<number, { element?: RenderElement; art?: string; note?: string }>
export type Fold = (id: string, hidden: number, key: string) => { folded: boolean; element: RenderElement } | null
export type Fold = (id: string, hidden: number, key: string, label?: string) =>{ folded: boolean; element: RenderElement } | null
export type Tip = (note: number) => (() => void) | undefined

export const instanceKey = (id: string): string => {
Expand Down Expand Up @@ -563,7 +527,7 @@ export const renderBlocks = (el: ElementTable, style: Style, blocks: Block[], co
return { first, second, html }
})
const beside = (b: number) => (hover || !buttons[b]!.first ? columns : columns - reserved(buttons[b]!.first) - 2)
const shown = blocks.map(block => drawable(block, style.httpLinks === true, style.editorLinks === true))
const shown = blocks.map(block => drawable(block, style))
const rendered = shown.map((block, b) => {
const key = `b${b}`
switch (block.kind) {
Expand Down
15 changes: 13 additions & 2 deletions plugins/cc-dazzle/hooks/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ const stamp = (t: number, span: number, sameYear: boolean) => {
return span < 2 * DAY ? iso.slice(11, 16) : sameYear ? iso.slice(5, 10) : iso.slice(0, 10)
}

const EIGHTHS = ' ▏▎▍▌▋▊▉'

const fineBar = (from: number, to: number, fallback: number) => {
const half = from % 1 >= 0.25 && from % 1 < 0.75
const start = half ? Math.floor(from) + 1 : Math.round(from)
const eighths = Math.round(to * 8)
const whole = Math.floor(eighths / 8) - start
if (whole < 1) return ' '.repeat(fallback) + '█'
return ' '.repeat(start - (half ? 1 : 0)) + (half ? '▐' : '') + '█'.repeat(whole) + (eighths % 8 > 1 ? EIGHTHS[eighths % 8]! : '')
}

export const ganttText = (source: string, ascii: boolean, columns: number): string | null => {
const [first = '', ...rest] = body(source)
if (!/^gantt$/i.test(first)) return null
Expand Down Expand Up @@ -200,8 +211,8 @@ export const ganttText = (source: string, ascii: boolean, columns: number): stri
const lines = rows.map(r => {
if (!('name' in r)) return r.section
const a = Math.min(col(r.start), chart - 1)
const cells = r.milestone ? (ascii ? '*' : '◆') : (ascii ? '#' : '█').repeat(Math.max(1, col(r.end) - a))
return `${pad(` ${r.name}`, labelWidth)} ${' '.repeat(a)}${cells}`
const cells = r.milestone ? (ascii ? '*' : '◆') : ascii ? '#'.repeat(Math.max(1, col(r.end) - a)) : null
return `${pad(` ${r.name}`, labelWidth)} ${cells === null ? fineBar(((r.start - min) / span) * chart, ((r.end - min) / span) * chart, a) : ' '.repeat(a) + cells}`
})
return [...(title ? [title.trim()] : []), lead + labels.join('').trimEnd(), lead + axis.join(''), ...lines].join('\n')
}
Loading
Loading