diff --git a/blog/src/_data/media.json b/blog/src/_data/media.json index 31ec3eced..ebe12f3cd 100644 --- a/blog/src/_data/media.json +++ b/blog/src/_data/media.json @@ -4746,5 +4746,16 @@ }, "inline": {}, "youtube": [] + }, + "claude-code-projects": { + "title": "Claude Code Projects: what they are and who can use them", + "category": "concepts", + "hero": { + "file": "assets/media/claude-code-projects/hero.png", + "alt": "Title card on white. The words Claude Code Projects in large ink type with a Concepts kicker, Checked 11 Oct 2026, and three spools of thread with their threads running side by side", + "prompt": "Still of the hero scene in assets/media/claude-code-projects/motion.js.", + "status": "ready" + }, + "inline": {} } } diff --git a/blog/src/assets/media/claude-code-projects/hero.png b/blog/src/assets/media/claude-code-projects/hero.png new file mode 100644 index 000000000..1148d2585 Binary files /dev/null and b/blog/src/assets/media/claude-code-projects/hero.png differ diff --git a/blog/src/assets/media/claude-code-projects/motion.css b/blog/src/assets/media/claude-code-projects/motion.css new file mode 100644 index 000000000..6b0880607 --- /dev/null +++ b/blog/src/assets/media/claude-code-projects/motion.css @@ -0,0 +1,13 @@ +/* Bright coded scenes for "Claude Code Projects". Pairs with motion.js. */ +.prose figure.mg { margin: 42px 0; } +.prose figure.mg img, .mg .mg-svg { display: block; width: 100%; height: auto; border: 1px solid #1A1320; border-radius: 18px; background: #fff; } +.mg figcaption { font: 500 13px/1.5 "Space Grotesk", system-ui, sans-serif; color: #7A6A88; padding-top: 10px; text-align: center; } +.mg .mg-svg { touch-action: manipulation; -webkit-tap-highlight-color: transparent; } +.mg .mg-svg text { -webkit-user-select: none; user-select: none; pointer-events: none; } +.mg .mg-hit { cursor: pointer; outline: none; } +.mg [role="slider"] { cursor: grab; touch-action: none; } +.mg .mg-ring { stroke-opacity: 0; transition: stroke-opacity 0.15s; } +.mg .mg-hit:focus-visible .mg-ring { stroke-opacity: 1; } +.mg [role="slider"]:focus-visible circle:nth-child(2) { stroke-width: 5; } +@media (hover: hover) { .mg .mg-hit:hover .mg-ring { stroke-opacity: 0.5; } } +@media (prefers-reduced-motion: reduce) { .mg .mg-ring { transition: none; } } diff --git a/blog/src/assets/media/claude-code-projects/motion.js b/blog/src/assets/media/claude-code-projects/motion.js new file mode 100644 index 000000000..dc3aa9859 --- /dev/null +++ b/blog/src/assets/media/claude-code-projects/motion.js @@ -0,0 +1,291 @@ +/* Bright coded scenes for "Claude Code Projects". Vanilla JS, no library. +
holds a still image; this script swaps in a live SVG. + Plays only in view, honours reduced motion, "?mgstill" freezes each scene on its still frame. */ +(() => { + const NS = "http://www.w3.org/2000/svg"; + const INK = "#1A1320", YEL = "#FFCA54", BLUE = "#6C8EF5", LIL = "#B69CFF", MINT = "#7FD8AE", SKY = "#9AD8FF", SOFT = "#F1ECF9", MUTE = "#7A6A88"; + const SANS = '"Space Grotesk", system-ui, sans-serif', MONO = '"JetBrains Mono", ui-monospace, monospace'; + const el = (t, a = {}, p) => { const n = document.createElementNS(NS, t); for (const k in a) n.setAttribute(k, a[k]); if (p) p.appendChild(n); return n; }; + const txt = (p, x, y, s, a = {}) => { const n = el("text", Object.assign({ x, y, "font-family": SANS, "font-size": 24, "font-weight": 600, fill: INK }, a), p); n.textContent = s; return n; }; + const cl = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v)); + const seg = (t, a, b) => cl((t - a) / (b - a)); + const oc = (x) => 1 - Math.pow(1 - x, 3); + const ob = (x) => { const c = 1.70158; return 1 + (c + 1) * Math.pow(x - 1, 3) + c * Math.pow(x - 1, 2); }; + const io = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2); + const tf = (n, x, y, s = 1, r = 0) => n.setAttribute("transform", `translate(${x} ${y}) rotate(${r}) scale(${s})`); + const button = (g, label, fn) => { + g.setAttribute("role", "button"); g.setAttribute("tabindex", "0"); g.setAttribute("aria-label", label); g.classList.add("mg-hit"); + g.addEventListener("click", fn); + g.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); fn(); } }); + }; + const strip = (svg, y, w = 680) => { + el("rect", { x: (800 - w) / 2, y, width: w, height: 48, rx: 24, fill: SOFT }, svg); + return txt(svg, 400, y + 31, "", { "text-anchor": "middle", "font-size": 20, "font-weight": 500 }); + }; + + const SC = {}; + + /* ---- 1. envelopes go in one wide slot and come out in a pigeonhole each ---- */ + SC.pigeonholes = { + dur: 12, still: 9.6, + build(svg) { + txt(svg, 44, 62, "One conversation, a thread for each task", { "font-size": 32, "font-weight": 700 }); + const SX = 540, X0 = 60, CW = 170, TOP = 220, BOT = 462, COL = [YEL, SKY, LIL, MINT]; + const defs = el("defs", {}, svg); + el("rect", { x: 0, y: 0, width: 800, height: 181 }, el("clipPath", { id: "mgc-pg-in" }, defs)); + el("rect", { x: X0, y: TOP + 2, width: CW * 4, height: BOT - TOP - 2 }, el("clipPath", { id: "mgc-pg-out" }, defs)); + el("rect", { x: X0, y: 146, width: CW * 4, height: 74, rx: 18, fill: SOFT, stroke: INK, "stroke-width": 3 }, svg); + txt(svg, X0 + 30, 193, "conversation", { "font-size": 28, "font-weight": 700 }); + el("rect", { x: SX - 150, y: 174, width: 300, height: 18, rx: 9, fill: INK }, svg); + el("rect", { x: X0, y: TOP, width: CW * 4, height: BOT - TOP, rx: 18, fill: "#fff", stroke: INK, "stroke-width": 3 }, svg); + [1, 2, 3].forEach((i) => el("path", { d: `M${X0 + CW * i} ${TOP} V${BOT}`, stroke: INK, "stroke-width": 3 }, svg)); + const env = (parent, col) => { + const g = el("g", {}, parent); + el("rect", { x: -54, y: -36, width: 108, height: 72, rx: 9, fill: col, stroke: INK, "stroke-width": 3 }, g); + el("path", { d: "M-54 -32 L0 2 L54 -32", stroke: INK, "stroke-width": 3, fill: "none", "stroke-linejoin": "round" }, g); + txt(g, 0, 27, "task", { "text-anchor": "middle", "font-size": 20, "font-weight": 700 }); + return g; + }; + const gin = el("g", { "clip-path": "url(#mgc-pg-in)" }, svg), gout = el("g", { "clip-path": "url(#mgc-pg-out)" }, svg); + const items = COL.map((c, i) => { + const cx = X0 + CW * i + CW / 2; + txt(svg, cx, BOT - 20, "thread", { "text-anchor": "middle", "font-size": 24, "font-weight": 700 }); + const k = el("g", {}, svg); + el("circle", { r: 16, fill: MINT, stroke: INK, "stroke-width": 3 }, k); + el("path", { d: "M-7 0 L-2 6 L8 -6", stroke: INK, "stroke-width": 3, fill: "none", "stroke-linecap": "round", "stroke-linejoin": "round" }, k); + return { a: env(gin, c), b: env(gout, c), k, cx, t0: 0.5 + i * 1.9 }; + }); + const cap = strip(svg, 496, 720); + return (t) => { + const out = 1 - seg(t, 11.2, 11.8); + items.forEach((m, i) => { + const a = m.t0, pop = ob(seg(t, a, a + 0.35)), drop = seg(t, a + 0.55, a + 1.0); + tf(m.a, SX, 112 + 110 * drop * drop, cl(pop, 0, 1.2)); + m.a.setAttribute("opacity", t < a ? 0 : 1); + const e = oc(seg(t, a + 1.05, a + 1.7)), bob = t > 8.6 ? Math.sin(t * 5 + i * 1.6) * 4 * seg(t, 8.6, 9) : 0; + tf(m.b, m.cx, TOP - 44 + 142 * e + bob); + m.b.setAttribute("opacity", out); + const p = ob(seg(t, a + 1.7, a + 2.0)); + tf(m.k, m.cx + 58, TOP + 28, cl(p, 0, 1.3)); + m.k.setAttribute("opacity", (p <= 0.01 ? 0 : 1) * out); + }); + cap.textContent = t > 8.6 && t < 11.2 ? "Parallel threads, usually in the cloud" : "Claude starts a thread for each task"; + }; + }, + }; + + /* ---- 2. four mugs on a shelf: two fill and steam, two stay upside down ---- */ + SC.mugs = { + dur: 12, still: 9.4, + build(svg, api) { + txt(svg, 44, 62, "Who can use Projects today?", { "font-size": 32, "font-weight": 700 }); + const SH = 404, MX = [130, 310, 490, 670], NAME = ["Pro", "Max", "Team", "Enterprise"]; + const LINES = ["Pro: public beta, rolling out gradually", "Max: public beta, rolling out gradually", "Team: not available yet", "Enterprise: not available yet"]; + const defs = el("defs", {}, svg), MUG = "M-58 -122 H58 L46 -16 Q44 0 28 0 H-28 Q-44 0 -46 -16 Z"; + el("path", { d: MUG }, el("clipPath", { id: "mgc-mug" }, defs)); + el("rect", { x: 40, y: SH, width: 720, height: 16, rx: 8, fill: SOFT, stroke: INK, "stroke-width": 3 }, svg); + const tag = (cx, w, fill, l1, l2) => { + const g = el("g", {}, svg), h = l2 ? 76 : 50; + el("rect", { x: -w / 2, y: -h / 2, width: w, height: h, rx: 22, fill, stroke: INK, "stroke-width": 3 }, g); + txt(g, 0, l2 ? -6 : 8, l1, { "text-anchor": "middle", "font-size": 24, "font-weight": 700 }); + if (l2) txt(g, 0, 22, l2, { "text-anchor": "middle", "font-size": 20, "font-weight": 500 }); + return g; + }; + const TA = tag(220, 290, MINT, "public beta", "rolling out gradually"), TB = tag(580, 170, SOFT, "not yet"); + let pinned = -1; + const mugs = MX.map((x, i) => { + const up = i < 2, g = el("g", {}, svg), m = el("g", {}, g); + el("rect", { x: x - 78, y: SH - 138, width: 172, height: 196, rx: 18, fill: "#fff", "fill-opacity": 0, stroke: INK, "stroke-width": 2, class: "mg-ring" }, g); + el("path", { d: "M48 -96 H66 a25 25 0 0 1 0 50 H48", stroke: INK, "stroke-width": 3, fill: "none", "stroke-linecap": "round" }, m); + el("path", { d: MUG, fill: up ? "#fff" : SOFT }, m); + const liq = up ? el("rect", { x: -60, y: 0, width: 120, height: 0, fill: YEL, "clip-path": "url(#mgc-mug)" }, m) : null; + el("path", { d: MUG, fill: "none", stroke: INK, "stroke-width": 3, "stroke-linejoin": "round" }, m); + const steam = up ? [-18, 18].map((dx) => el("path", { d: `M${dx} 0 c-12 -12 12 -22 0 -34 c-12 -12 12 -22 0 -34`, stroke: LIL, "stroke-width": 4, fill: "none", "stroke-linecap": "round" }, g)) : []; + txt(g, x, SH + 52, NAME[i], { "text-anchor": "middle", "font-size": 28, "font-weight": 700 }); + button(g, LINES[i], () => { pinned = i; api.poke(); }); + return { m, liq, steam, x, up }; + }); + const cap = strip(svg, 496, 720); + return (t) => { + const out = 1 - seg(t, 11.2, 11.8); + const pa = ob(seg(t, 0.4, 0.8)), pb = ob(seg(t, 6.0, 6.4)); + tf(TA, 220, 132, cl(pa, 0, 1.2)); TA.setAttribute("opacity", (pa <= 0.01 ? 0 : 1) * out); + tf(TB, 580, 190, cl(pb, 0, 1.2)); TB.setAttribute("opacity", (pb <= 0.01 ? 0 : 1) * out); + const fill = io(seg(t, 1.2, 5.2)) * out; + mugs.forEach((k, i) => { + if (k.up) { + tf(k.m, k.x, SH); + const h = 100 * fill; k.liq.setAttribute("y", -h); k.liq.setAttribute("height", h); + k.steam.forEach((s, j) => { + const ph = (t * 0.45 + j * 0.5 + i * 0.25) % 1; + s.setAttribute("transform", `translate(${k.x} ${SH - 136 - 14 * ph})`); + s.setAttribute("opacity", seg(t, 4.2, 5.2) * out * Math.sin(ph * Math.PI) * 0.9); + }); + } else { + const j = Math.sin(seg(t, 6.2 + (i - 2) * 0.3, 6.9 + (i - 2) * 0.3) * Math.PI * 3) * 3; + k.m.setAttribute("transform", `translate(${k.x} ${SH - 122}) rotate(${j}) scale(1 -1)`); + } + }); + cap.textContent = pinned >= 0 ? LINES[pinned] : t > 6 && t < 11.2 ? "Not available on Team or Enterprise plans yet" : "Public beta on Pro and Max plans"; + }; + }, + }; + + /* ---- 3. a noticeboard with six headings; thread cards unpin and move across ---- */ + SC.noticeboard = { + dur: 12, still: 10.7, + build(svg) { + txt(svg, 44, 62, "Six groups on the Threads tab", { "font-size": 32, "font-weight": 700 }); + const BX = 30, BW = 740, BY = 92, BH = 362, CWD = BW / 6, CX = (c) => BX + CWD * c + CWD / 2, SY = (s) => 214 + s * 67; + el("rect", { x: BX, y: BY, width: BW, height: BH, rx: 18, fill: SOFT, stroke: INK, "stroke-width": 3 }, svg); + el("rect", { x: BX, y: BY, width: BW, height: 84, rx: 18, fill: "#fff", stroke: INK, "stroke-width": 3 }, svg); + [1, 2, 3, 4, 5].forEach((i) => el("path", { d: `M${BX + CWD * i} ${BY + 96} V${BY + BH - 12}`, stroke: LIL, "stroke-width": 2, "stroke-dasharray": "2 8", "stroke-linecap": "round" }, svg)); + [["Ready for", "review"], ["Waiting", "on you"], ["Working"], ["Landing"], ["Idle"], ["Resolved"]].forEach((h, c) => { + h.forEach((s, j) => txt(svg, CX(c), BY + (h.length > 1 ? 36 + j * 26 : 50), s, { "text-anchor": "middle", "font-size": 20, "font-weight": 700 })); + }); + const COL = [YEL, SKY, LIL, MINT, "#fff", "#fff"], HOME = [[2, 0], [2, 1], [2, 2], [2, 3], [4, 0], [4, 1]]; + /* each card: list of [start, end, col, slot] hops after its place in Working */ + const HOPS = [[[2.0, 3.0, 0, 0]], [[3.6, 4.6, 1, 0]], [[5.2, 6.2, 3, 0], [7.4, 8.4, 5, 0]], [[8.6, 9.2, 2, 0]], [], [[9.5, 10.2, 2, 1]]]; + const cards = COL.map((c, i) => { + const g = el("g", {}, svg); + el("rect", { x: -50, y: -28, width: 100, height: 56, rx: 8, fill: c, stroke: INK, "stroke-width": 3 }, g); + txt(g, 0, 17, "thread", { "text-anchor": "middle", "font-size": 22, "font-weight": 700 }); + const pin = el("circle", { cx: 0, cy: -17, r: 6, fill: BLUE, stroke: INK, "stroke-width": 3 }, g); + return { g, pin, i }; + }); + const cap = strip(svg, 486, 720); + return (t) => { + const out = 1 - seg(t, 11.4, 11.9); + cards.forEach((k) => { + let x = CX(HOME[k.i][0]), y = SY(HOME[k.i][1]), lift = 0; + HOPS[k.i].forEach(([a, b, c, s]) => { + const p = io(seg(t, a, b)), nx = CX(c), ny = SY(s); + x += (nx - x) * p; y += (ny - y) * p; + lift = Math.max(lift, Math.sin(seg(t, a - 0.25, b + 0.15) * Math.PI)); + }); + const pop = ob(seg(t, 0.3 + k.i * 0.15, 0.7 + k.i * 0.15)); + const wob = k.i === 1 && t > 4.7 ? Math.sin((t - 4.7) * 9) * 6 * seg(t, 4.7, 5.0) : 0; + k.g.setAttribute("transform", `translate(${x} ${y - 14 * lift - 22}) rotate(${wob}) translate(0 22) scale(${cl(pop, 0, 1.2) * (1 + 0.08 * lift)})`); + k.g.setAttribute("opacity", (pop <= 0.01 ? 0 : 1) * out); + k.pin.setAttribute("opacity", lift > 0.12 ? 0 : 1); + }); + cap.textContent = t > 4.6 && t < 11.4 ? "See the threads at once and which of them need you" : "A project is one conversation plus the threads it starts"; + }; + }, + }; + + /* ---- 4. one watering can, first one pot, then three: the level drops faster ---- */ + SC.watering = { + dur: 12, still: 9.6, + build(svg, api) { + txt(svg, 44, 62, "The same plan limits, used faster", { "font-size": 32, "font-weight": 700 }); + const CXC = 176, CYC = 262, G = 446, PX = [468, 592, 716], TIP = [356, 250]; + const defs = el("defs", {}, svg); + el("rect", { x: -88, y: -68, width: 176, height: 136, rx: 16 }, el("clipPath", { id: "mgc-can" }, defs)); + /* toggle */ + const tog = el("g", {}, svg); + el("rect", { x: 440, y: 90, width: 330, height: 56, rx: 28, fill: "#fff", "fill-opacity": 0, stroke: INK, "stroke-width": 2, class: "mg-ring" }, tog); + el("rect", { x: 446, y: 96, width: 318, height: 44, rx: 22, fill: "#fff", stroke: INK, "stroke-width": 3 }, tog); + const knob = el("rect", { y: 96, height: 44, rx: 22, fill: YEL, stroke: INK, "stroke-width": 3 }, tog); + txt(tog, 538, 125, "single session", { "text-anchor": "middle", "font-size": 20, "font-weight": 700 }); + txt(tog, 697, 125, "project", { "text-anchor": "middle", "font-size": 20, "font-weight": 700 }); + let tnow = 0; + button(tog, "Switch between a single session and a project", () => api.seek(tnow < 5 ? 5.2 : 0.2)); + /* streams, drawn behind the pots */ + const streams = PX.map((x) => el("path", { d: `M${TIP[0]} ${TIP[1]} Q${(TIP[0] + x) / 2 + 30} ${TIP[1] - 90 - (x - 468) * 0.25} ${x} ${G - 120}`, stroke: SKY, "stroke-width": 6, fill: "none", "stroke-linecap": "round", "stroke-dasharray": "2 16" }, svg)); + el("path", { d: `M30 ${G} H770`, stroke: INK, "stroke-width": 3, "stroke-linecap": "round" }, svg); + /* can */ + const can = el("g", {}, svg); + el("path", { d: "M-88 -40 C-150 -40 -150 40 -88 40", stroke: INK, "stroke-width": 3, fill: "none", "stroke-linecap": "round" }, can); + el("path", { d: "M86 4 L168 -74 L188 -56 L86 52 Z", fill: "#fff", stroke: INK, "stroke-width": 3, "stroke-linejoin": "round" }, can); + el("rect", { x: -88, y: -68, width: 176, height: 136, rx: 16, fill: "#fff" }, can); + const water = el("rect", { x: -88, width: 176, fill: SKY, "clip-path": "url(#mgc-can)" }, can); + el("rect", { x: -88, y: -68, width: 176, height: 136, rx: 16, fill: "none", stroke: INK, "stroke-width": 3 }, can); + txt(svg, CXC, G - 44, "plan limits", { "text-anchor": "middle", "font-size": 26, "font-weight": 700 }); + const pots = PX.map((x, i) => { + const g = el("g", {}, svg), plant = el("g", {}, g); + el("path", { d: "M0 0 V-34 M0 -16 C-26 -16 -30 -40 -30 -40 C-8 -42 0 -30 0 -16 M0 -26 C22 -26 28 -52 28 -52 C8 -52 0 -40 0 -26", stroke: INK, "stroke-width": 3, fill: MINT, "stroke-linejoin": "round", "stroke-linecap": "round" }, plant); + plant.setAttribute("transform", "translate(0 -84)"); + el("path", { d: "M-40 -70 H40 L30 0 H-30 Z", fill: LIL, stroke: INK, "stroke-width": 3, "stroke-linejoin": "round" }, g); + el("rect", { x: -46, y: -86, width: 92, height: 18, rx: 6, fill: LIL, stroke: INK, "stroke-width": 3 }, g); + const lab = txt(g, 0, 34, i ? "thread" : "session", { "text-anchor": "middle", "font-size": 24, "font-weight": 700 }); + return { g, plant, lab, x }; + }); + const cap = strip(svg, 506, 720); + return (t) => { + tnow = t; + const out = 1 - seg(t, 11.3, 11.9), proj = t >= 5, tilt = io(seg(t, 0.3, 1.0)) * out; + const level = 1 - 0.1 * seg(t, 1.0, 5.0) - 0.68 * seg(t, 6.0, 11.0) + 0.78 * io(seg(t, 11.3, 11.9)); + const h = 124 * cl(level); + water.setAttribute("y", 68 - h); water.setAttribute("height", h); + tf(can, CXC, CYC - 6 * tilt, 1, 14 * tilt); + const kp = io(seg(t, 5.0, 5.5)) - io(seg(t, 11.5, 12)); + knob.setAttribute("x", 446 + 184 * kp); knob.setAttribute("width", 184 - 50 * kp); + pots.forEach((p, i) => { + const inn = i ? oc(seg(t, 5.1 + i * 0.25, 5.8 + i * 0.25)) * out : 1; + tf(p.g, p.x + (1 - inn) * 120, G); + p.g.setAttribute("opacity", i ? cl(inn * 1.5) : 1); + if (!i) p.lab.textContent = proj && t < 11.6 ? "thread" : "session"; + const on = (i ? seg(t, 6.0 + i * 0.2, 6.4 + i * 0.2) : seg(t, 1.0, 1.4)) * out; + streams[i].setAttribute("opacity", on); + streams[i].setAttribute("stroke-dashoffset", -t * 60); + const grow = i ? 0.55 + 0.45 * seg(t, 6.2, 11) : 0.55 + 0.45 * seg(t, 1.2, 9); + p.plant.setAttribute("transform", `translate(0 -84) scale(${grow})`); + }); + cap.textContent = proj && t < 11.6 ? "A project uses your limits faster than a single session does" : "A single session"; + }; + }, + }; + + /* ---- title card, 16 by 9: three spools, three threads running side by side ---- */ + SC.hero = { + w: 960, h: 540, dur: 1, still: 0, + build(svg) { + txt(svg, 64, 112, "CONCEPTS", { "font-family": MONO, "font-size": 20, "font-weight": 700, fill: MUTE, "letter-spacing": 3 }); + txt(svg, 64, 200, "Claude Code", { "font-size": 76, "font-weight": 700 }); + txt(svg, 64, 286, "Projects", { "font-size": 76, "font-weight": 700 }); + el("path", { d: "M66 312 H372", stroke: YEL, "stroke-width": 14, "stroke-linecap": "round" }, svg); + txt(svg, 64, 370, "What they are and who can use them", { "font-size": 25, "font-weight": 500, fill: MUTE }); + el("rect", { x: 60, y: 408, width: 300, height: 48, rx: 24, fill: SOFT }, svg); + txt(svg, 210, 439, "Checked 11 Oct 2026", { "text-anchor": "middle", "font-family": MONO, "font-size": 20, "font-weight": 700 }); + [[YEL, 136], [BLUE, 270], [MINT, 404]].forEach(([c, y]) => { + el("path", { d: `M790 ${y + 14} C740 ${y - 30} 700 ${y + 50} 650 ${y + 10} S590 ${y - 20} 566 ${y + 8}`, stroke: c, "stroke-width": 7, fill: "none", "stroke-linecap": "round" }, svg); + el("rect", { x: 786, y: y - 34, width: 84, height: 68, fill: c, stroke: INK, "stroke-width": 3 }, svg); + el("path", { d: `M786 ${y - 17} H870 M786 ${y} H870 M786 ${y + 17} H870`, stroke: INK, "stroke-width": 3 }, svg); + [y - 50, y + 34].forEach((yy) => el("rect", { x: 770, y: yy, width: 116, height: 16, rx: 8, fill: SOFT, stroke: INK, "stroke-width": 3 }, svg)); + }); + return () => {}; + }, + }; + + /* ---- player ---- */ + const q = new URLSearchParams(location.search), frozen = q.has("mgstill"), fixed = parseFloat(q.get("mgstill")); + const calm = matchMedia("(prefers-reduced-motion: reduce)").matches; + const mount = (fig) => { + const sc = SC[fig.dataset.scene], img = fig.querySelector("img"); if (!sc || !img) return; + const W = sc.w || 800, H = sc.h || 600; + const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, class: "mg-svg", role: "group", "aria-label": img.alt || "" }); + svg.style.aspectRatio = `${W} / ${H}`; + el("rect", { width: W, height: H, fill: "#fff" }, svg); + img.replaceWith(svg); + let off = 0, raf = 0, seen = false, held = calm; + const base = performance.now(), clock = () => (performance.now() - base) / 1000; + let draw = () => {}; + const frame = () => { const now = clock(); draw(held ? sc.still : (((now + off) % sc.dur) + sc.dur) % sc.dur, now); }; + const tick = () => { cancelAnimationFrame(raf); frame(); if (seen && !held) raf = requestAnimationFrame(tick); }; + const api = { now: clock, seek: (s) => { off = s - clock(); held = false; tick(); }, poke: () => { held = false; tick(); } }; + draw = sc.build(svg, api); + if (frozen) { draw(isNaN(fixed) ? sc.still : fixed, 0); return; } + new IntersectionObserver(([e]) => { + seen = e.isIntersecting; + if (seen) { tick(); if (!fig.dataset.sent && window.posthog) { fig.dataset.sent = 1; window.posthog.capture("blog_scene_view", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } } + else cancelAnimationFrame(raf); + }, { threshold: 0.25 }).observe(fig); + svg.addEventListener("click", () => { if (!fig.dataset.hit && window.posthog) { fig.dataset.hit = 1; window.posthog.capture("blog_scene_interact", { slug: location.pathname.split("/").filter(Boolean).pop(), scene: fig.dataset.scene }); } }); + frame(); + }; + const go = () => document.querySelectorAll("figure.mg[data-scene]").forEach(mount); + const fonts = document.fonts && document.fonts.load ? Promise.all([document.fonts.load('700 32px "Space Grotesk"'), document.fonts.load('500 20px "Space Grotesk"'), document.fonts.load('700 20px "JetBrains Mono"')]).catch(() => {}) : Promise.resolve(); + const start = () => fonts.then(go, go); + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); else start(); +})(); diff --git a/blog/src/assets/media/claude-code-projects/mugs.png b/blog/src/assets/media/claude-code-projects/mugs.png new file mode 100644 index 000000000..d89ec5377 Binary files /dev/null and b/blog/src/assets/media/claude-code-projects/mugs.png differ diff --git a/blog/src/assets/media/claude-code-projects/noticeboard.png b/blog/src/assets/media/claude-code-projects/noticeboard.png new file mode 100644 index 000000000..b89e35cef Binary files /dev/null and b/blog/src/assets/media/claude-code-projects/noticeboard.png differ diff --git a/blog/src/assets/media/claude-code-projects/pigeonholes.png b/blog/src/assets/media/claude-code-projects/pigeonholes.png new file mode 100644 index 000000000..91f8f1ce6 Binary files /dev/null and b/blog/src/assets/media/claude-code-projects/pigeonholes.png differ diff --git a/blog/src/assets/media/claude-code-projects/watering.png b/blog/src/assets/media/claude-code-projects/watering.png new file mode 100644 index 000000000..dd403d8bb Binary files /dev/null and b/blog/src/assets/media/claude-code-projects/watering.png differ diff --git a/blog/src/posts/claude-code-agent-teams.md b/blog/src/posts/claude-code-agent-teams.md index 2b0d05da2..fb1d076ec 100644 --- a/blog/src/posts/claude-code-agent-teams.md +++ b/blog/src/posts/claude-code-agent-teams.md @@ -107,6 +107,6 @@ Our view: a team is worth it when the work splits into independent pieces that g On size, the docs say to start with 3 to 5 teammates for most workflows, and that "three focused teammates often outperform five scattered ones". We think a PR review is the right first run. A team of five to rename a variable is a meeting that should have been an email. -To run separate sessions by hand, see [managing multiple Claude Code sessions](/blog/manage-multiple-claude-code-sessions/). +To run separate sessions by hand, see [managing multiple Claude Code sessions](/blog/manage-multiple-claude-code-sessions/). See also [Claude Code Projects](/blog/claude-code-projects/). diff --git a/blog/src/posts/claude-code-projects.md b/blog/src/posts/claude-code-projects.md new file mode 100644 index 000000000..f6289c018 --- /dev/null +++ b/blog/src/posts/claude-code-projects.md @@ -0,0 +1,110 @@ +--- +title: "Claude Code Projects: what they are and who can use them" +description: "Claude Code Projects, checked 11 Oct 2026: a public beta on Pro and Max where Claude runs your tasks as parallel threads. Access, limits and plan use." +date: 2026-10-11 +category: concepts +categoryLabel: Concepts +type: Non-technical +primaryKeyword: "claude code projects" +secondaryKeywords: ["claude projects", "claude code projects waitlist", "claude code cloud sessions", "claude code threads", "claude code projects pricing"] +tags: ["Concepts", "Claude Code", "AI Agents"] +faq: + - q: "What are Claude Code Projects?" + a: "Anthropic's Projects docs describe a project as one ongoing conversation where Claude coordinates a stream of related work for you. You tell it what needs doing and it starts a thread for each task. The docs say each thread is usually a cloud session." + - q: "Are Claude Code Projects available on Team or Enterprise plans?" + a: "Not yet. On 11 Oct 2026 Anthropic's Projects docs said Projects are in public beta on Pro and Max plans and rolling out gradually, and that they aren't available on Team or Enterprise plans yet. If Projects doesn't appear in your sidebar, the docs point to a waitlist." + - q: "How much do Claude Code Projects cost?" + a: "Anthropic's Projects docs give no separate price. They say project usage counts against the same plan limits as your other Claude Code sessions, and that a project uses your limits faster than a single session does. Work goes past your plan's limits only if you have turned on usage credits." + - q: "Can a project thread run on my own computer?" + a: "Yes, when you ask for it. Anthropic's Projects docs say the thread is then a Claude Code session in a folder on your machine, connected through Remote Control, and that it runs only while that computer is awake with Remote Control turned on. The docs say this needs Claude Code v2.1.280 or later." + - q: "Are these the same as Projects in claude.ai chat?" + a: "No. Anthropic's Projects docs call the chat and Cowork version the earlier Projects experience, which groups conversations and reference files without threads or a coordinator. The docs say those projects keep working as they do today until the redesigned experience reaches them." +--- + +Projects in Claude Code are one ongoing conversation where Claude runs your tasks as parallel threads, usually in the cloud. They are in public beta on Pro and Max plans and rolling out gradually. Our view: join the waitlist if your code is on github.com, and expect to use your plan faster. Checked 11 Oct 2026. + +We read Anthropic's [Projects docs](https://code.claude.com/docs/en/claude-projects), [launch post](https://claude.com/blog/projects-redesigned) and [changelog page](https://code.claude.com/docs/en/changelog) on 11 Oct 2026. We have not tried Projects. The [concepts hub](/blog/topics/concepts/) explains the terms. + +[Munder Difflin](https://harnessmd.com/download) is free and open source: a desktop app that runs a team of coding agents such as Claude Code, Codex and Gemini CLI on your own computer. It is not part of Projects. The [install guide](/blog/how-to-install-and-use-munder-difflin/) covers it. + +## What are Projects in Claude Code? + +A project is one ongoing conversation where Claude coordinates a stream of related work and starts a thread for each task. The Projects docs say each thread is usually a cloud session, "Claude Code running in the cloud rather than on your machine", that threads run in parallel, and that cloud threads keep going after you close your laptop. + +Anthropic's launch post is dated September 17, 2026 and calls the feature "a new experience for Claude projects, now available in beta in Claude Code". + +
Animation. A post office sorting rack of pigeonholes. Envelopes labelled task drop one by one into a wide slot at the top labelled conversation, then each slides down into its own pigeonhole labelled thread, and a small tick pops up on each pigeonhole as it fills.
One conversation, a thread for each task. Anthropic's Projects docs, read 11 Oct 2026.
+ +## Who can use Projects today? + +Pro and Max subscribers whose accounts the rollout has reached. On 11 Oct 2026 the note at the top of the Projects docs said Projects are "in public beta on Pro and Max plans and rolling out gradually, starting with accounts that have used cloud sessions and don't have existing projects in claude.ai chat or Cowork". The Projects docs add that they "aren't available on Team or Enterprise plans yet". + +If Projects doesn't appear in the sidebar at claude.ai/code or in the Code tab of the desktop app, the Projects docs say the rollout hasn't reached your account, and that you can [join the waitlist](https://claude.com/form/projects). + +The Projects docs say Projects are not in the terminal CLI, the VS Code extension or the JetBrains plugin. + +
Animation. A row of four mugs on a shelf, labelled Pro, Max, Team and Enterprise. The Pro and Max mugs fill slowly and steam, with a tag reading public beta, rolling out gradually. The Team and Enterprise mugs stay upside down under a tag reading not yet.
Public beta on Pro and Max plans, rolling out gradually. Not available on Team or Enterprise plans yet. Anthropic's Projects docs, read 11 Oct 2026.
+ +## How is a project organized? + +A project is one coordinating conversation with Claude plus the threads it starts. The four rows are our selection from the Projects docs, read on 11 Oct 2026. + +| Part | What the Projects docs say | +| --- | --- | +| Project conversation | One long-running session where Claude acts as coordinator. It sees what threads report back, not every step they take. | +| Threads | Separate sessions, each with its own context window. A cloud thread works on its own branch. | +| Project instructions | Text sent to each new thread, up to 16,000 characters. | +| Overview pane | Where you see the threads at once and which of them need you. | + +The Projects docs list six groups on the Overview pane's Threads tab: Ready for review, Waiting on you, Working, Landing, Idle and Resolved. + +
Animation. A noticeboard with six column headings: Ready for review, Waiting on you, Working, Landing, Idle and Resolved. Paper cards labelled thread are pinned under Working and Idle, then unpin and move across to other columns one at a time, and the card under Waiting on you wobbles.
The Overview pane's Threads tab groups threads into six states. Anthropic's Projects docs, read 11 Oct 2026.
+ +## What do you need before you create one? + +You need a Pro or Max plan with Projects showing in your sidebar, and a GitHub setup if the project will work on code. The Projects docs ask for three things there: your code is on github.com "rather than GitHub Enterprise Server, GitLab, or Bitbucket", your connected GitHub account has push access to it, and the Claude GitHub App is installed on it. + +The Projects docs add that cloud threads don't pick up anything from the Claude Code setup on your own machine. + +## What does a project cost? + +The Projects docs give no price for Projects: usage "counts against the same plan limits as your other Claude Code sessions". The Projects docs say a project "uses your limits faster than a single session does", and that "on a Pro plan in particular you should expect to reach your limit sooner on days you run one". + +The Projects docs do give one count: "The enforced limit is 200 new threads per day across your projects." They also say a new project runs every thread on Opus at high effort, "which draws on your plan fastest". + +A thread that reaches your plan's limit usually waits and continues when the limit resets. The Projects docs say a thread started by a routine stops instead. So closing the laptop stops neither a cloud thread nor its usage. For plan prices, see [how much Claude Code costs](/blog/how-much-does-claude-code-cost/). + +
Animation. One watering can labelled plan limits tips over a single plant pot labelled session and its water level drops slowly. Then more pots labelled thread slide in beside it, the can waters them together, and the water level drops faster. A toggle switches between single session and project.
A project uses your limits faster than a single session does. Anthropic's Projects docs, read 11 Oct 2026, give no figure for how much faster.
+ +## What are the limits in beta? + +The Projects docs list six limitations. These four are our selection: + +* **One user.** "A project belongs to one user." You can't share a project or its threads with another user. +* **No organization controls.** There are no organization-level controls for projects during the beta. +* **Threads stay put.** You can't move or copy a thread to another project. +* **Uncommitted changes can be lost.** If a cloud thread's sandbox can't be resumed, the thread continues from a fresh clone. + +## How is this different from agent teams, agent view and routines? + +In a project, Claude starts and tracks the sessions instead of you. The Projects docs compare the neighbours this way: + +* **Agent teams.** "One session that starts teammate sessions for a single task", which "ends with that task". Our [agent teams explainer](/blog/claude-code-agent-teams/) has the detail. +* **Agent view.** "A screen for tracking several local sessions side by side", where you still start each one yourself. See [managing multiple Claude Code sessions](/blog/manage-multiple-claude-code-sessions/). +* **Routines.** When you ask for scheduled work in a project, Claude creates a routine that runs as threads in that project. See [Claude Code automation while you sleep](/blog/claude-code-automation-while-you-sleep/). + +## What does it mean if you run coding agents on your own computer? + +A project reaches your machine only by running a thread there through Remote Control, the Projects docs say. That needs Claude Code v2.1.280 or later on that computer, and the thread "runs only while that computer is awake with Remote Control turned on". The changelog page dates 2.1.280 to September 22, 2026. + +The Projects docs also say you can't add a session you started yourself on your machine to a project. For work where every task needs your machine, the Projects docs suggest a local session or agent view instead. Our comparison of [local-first and cloud agent SDKs](/blog/local-first-vs-cloud-agent-sdks/) covers that trade. + +## What should you do now? + +Check your sidebar first, then join the waitlist. + +1. Look for Projects at claude.ai/code or in the Code tab of the desktop app. +2. If it is missing and you are on Pro or Max, join the waitlist. +3. On your first project, check Thread model and Thread effort before you send a batch. + + diff --git a/docs/blog/agent-tools-today-2026-10-11/index.html b/docs/blog/agent-tools-today-2026-10-11/index.html index dbfaabcee..ce9f14c03 100644 --- a/docs/blog/agent-tools-today-2026-10-11/index.html +++ b/docs/blog/agent-tools-today-2026-10-11/index.html @@ -238,7 +238,7 @@

How this list is built + -