Skip to content
4 changes: 3 additions & 1 deletion KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,13 @@ Livestream history: the team agreed to build semantic search on [day two at 2:57

A **commit** is a post on /dev/null, not a Git commit in a repository. Open the composer or press **c**, write your update, and choose **Commit** or press **⌘/Ctrl+Enter**. Text-only commits accept 1–4,000 characters; as you type, the ring around your avatar in the composer fills toward that limit. On the web and in the app, an image attachment makes the text optional (terminal commits still need text), but the 4,000-character maximum still applies. Runs of blank lines outside code collapse to one before the length is checked. Lists fold posts longer than 280 characters or 8 lines behind **Show more**. When a commit starts with a code block too long to fit, its first lines fade out to show that the code continues. **Show more** reveals the full snippet, and the copy control copies the whole block even while folded. Commits can be created and deleted, but not edited after publication. There are no plans to add commit editing, and it will never be added: to change a commit, delete it and post a new one.

Typing `/` in the composer opens a slash command menu: `/image` attaches images (alias `/img`), `/poll` starts a poll, `/code` inserts a fenced code block, and `/bug` or `/feature` turn a fresh draft into a GitHub issue draft. Choose with ↑/↓ and Enter or Tab, or dismiss with Esc; an exact command followed by a space runs immediately. Commands a draft cannot use right now are hidden: `/poll`, `/bug`, and `/feature` are unavailable in replies and quotes, and issue commands are also hidden while scheduling. Commands that conflict with the current draft, such as images combined with polls, appear disabled with the reason.

Commits, replies, quotes, scheduling, terminal submissions, and app (signed API) commits share a limit of 10 attempts per minute per user. Posts go through [moderation](#moderation), so a newly submitted commit may be pending rather than public immediately.

Approved top-level commits about technology, current affairs or substantive real-world events can receive brief, shared subject headlines. Technical and developer topics carry the most weight. /dev/null community activity also qualifies, including livestreams, Tom, app betas and feature updates. Quote commits include the directly quoted post's text when it is approved and still available, so a brief reaction can share the subject of the post it quotes. Replies, casual small talk and evergreen lifestyle or hobby advice are skipped. OpenAI's Decisions API assesses this text for suitability and checks the closest existing headline names, shortlisted by semantic similarity; when none match, DeepSeek through OpenCode generates a headline from the same text. Posts can be processed together, and similar newly generated headlines share the first headline rather than creating duplicates. No account metadata is sent. A headline expires after fourteen days without a new commit link. This categorization does not change the original post and does not yet add visible headline labels to the interface.

Sources: [composer](resources/js/components/commit-composer.tsx), [commit model](app/Models/Commit.php), [commit validation](app/Http/Requests/CreateCommitRequest.php), [text normalization](app/Services/CommitBodies.php), [submission throttle](app/Http/Middleware/ThrottleCommits.php).
Sources: [composer](resources/js/components/commit-composer.tsx), [slash commands](resources/js/lib/slash-commands.ts), [commit model](app/Models/Commit.php), [commit validation](app/Http/Requests/CreateCommitRequest.php), [text normalization](app/Services/CommitBodies.php), [submission throttle](app/Http/Middleware/ThrottleCommits.php).

Livestream history: the team settled on calling posts **commits** and their list a **log** on [day one at 2:56:00](https://www.youtube.com/watch?v=6zCNQa3hNm0&t=10560s). At [3:02:19](https://www.youtube.com/watch?v=6zCNQa3hNm0&t=10939s), they agreed to rename the post model too, rather than changing only the UI. These are naming decisions, not timestamps proving the rename had finished.

Expand Down
2 changes: 1 addition & 1 deletion PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Developers who left X or Bluesky, or are thinking about it, and want a feed with

What exists, one line each (details in the knowledge base):

- **Commits:** text with code, images, link cards and embedded players, or a poll; replies, quotes, threads, scheduling, translation.
- **Commits:** text with code, images, link cards and embedded players, or a poll; slash commands for images, polls, code, and GitHub issue drafts; replies, quotes, threads, scheduling, translation.
- **Logs:** Following, Latest, and Trending.
- **Engagement:** follows, stars, saved commits, pins, mentions, and pastéis (an opt-in daily allowance of pastries).
- **Profiles:** mirrored from GitHub, with an activity graph, streaks, achievements, a Unix rank, and a live marker while you stream on YouTube.
Expand Down
4 changes: 2 additions & 2 deletions resources/js/components/commit-composer-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function CommitComposerDialog({
onEscapeKeyDown={(event) => {
if (
document.querySelector(
'[data-mention-suggestions], [data-command-suggestions], [role="dialog"].yarl__root',
'[data-mention-suggestions], [data-slash-suggestions], [data-emoji-suggestions], [data-issue-suggestions], [role="dialog"].yarl__root',
) ||
(typeof Element !== 'undefined' &&
event.target instanceof Element &&
Expand All @@ -91,7 +91,7 @@ export function CommitComposerDialog({
typeof Element !== 'undefined' &&
event.target instanceof Element &&
event.target.closest(
'[data-mention-suggestions], [data-command-suggestions], [role="dialog"].yarl__root',
'[data-mention-suggestions], [data-slash-suggestions], [data-emoji-suggestions], [data-issue-suggestions], [role="dialog"].yarl__root',
)
) {
event.preventDefault();
Expand Down
55 changes: 25 additions & 30 deletions resources/js/components/commit-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import type { UseIssueCommandReturn } from '@/components/issue-command';
import {
IssueCommandBadge,
IssueCommandHint,
IssueCommandSuggestions,
useIssueCommand,
} from '@/components/issue-command';
import { LinkCardPicker } from '@/components/link-card-picker';
Expand Down Expand Up @@ -159,9 +158,17 @@ function CommitComposerBody({
: undefined);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const textBox = useRef<HTMLDivElement>(null);
const imageInputRef = useRef<HTMLInputElement>(null);
const preview = useComposerPreview({ body, textareaRef, isSubmittable });
const isIssue = issue.command !== null;

const startPoll = () => {
setPoll(emptyPollDraft);
requestAnimationFrame(() =>
document.getElementById(`${id}-poll-option-0`)?.focus(),
);
};

// When the dialog's text box shrinks, as the keyboard opens or an image is added, it keeps the caret in view.
useEffect(() => {
const box = textBox.current;
Expand Down Expand Up @@ -296,17 +303,24 @@ function CommitComposerBody({
preview.handleShortcut(event) ||
onKeyDown(event)
}
onFocus={(event) => {
issue.focus();

if (isRestored) {
moveCaretToEnd(event);
}
}}
onBlur={issue.blur}
onFocus={isRestored ? moveCaretToEnd : undefined}
maxLength={
isIssue ? maxBodyLength : maxRawBodyLength
}
onImage={
isIssue
? undefined
: () => imageInputRef.current?.click()
}
onPoll={isIssue ? undefined : startPoll}
onIssue={(kind, rest) => issue.start(kind, rest)}
commandContext={{
surface: 'commit',
hasPoll: poll !== null,
imageCount: attachments.images.length,
isScheduling,
processing,
}}
placeholder={
issue.command?.placeholder ??
(poll ? 'Ask a question' : 'What did you ship?')
Expand Down Expand Up @@ -353,17 +367,6 @@ function CommitComposerBody({
/>
)}

{issue.suggestions.length > 0 && (
<IssueCommandSuggestions
suggestions={issue.suggestions}
selected={issue.selected}
onPick={(kind) => {
issue.start(kind, '');
focusBody(id);
}}
/>
)}

<CommitRulesDialog
rules={commitRules}
textarea={textareaRef}
Expand Down Expand Up @@ -462,6 +465,7 @@ function CommitComposerBody({
disabled={processing || poll !== null}
reading={attachments.reading}
onPick={attachments.pick}
inputRef={imageInputRef}
className="-ml-2"
/>
<ActionTooltip
Expand All @@ -481,16 +485,7 @@ function CommitComposerBody({
poll !== null ||
hasImages
}
onClick={() => {
setPoll(emptyPollDraft);
requestAnimationFrame(() =>
document
.getElementById(
`${id}-poll-option-0`,
)
?.focus(),
);
}}
onClick={startPoll}
aria-label="Add poll"
className="text-muted-foreground"
data-test="add-poll-button"
Expand Down
10 changes: 7 additions & 3 deletions resources/js/components/commit-image-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ImagePlus, X } from 'lucide-react';
import type { RefObject } from 'react';
import { useId, useLayoutEffect, useRef, useState } from 'react';
import { ActionTooltip } from '@/components/action-tooltip';
import type { ShownImage } from '@/components/commit-images';
Expand Down Expand Up @@ -112,19 +113,22 @@ export function ImagePickerButton({
reading = false,
onPick,
className,
inputRef,
}: {
count: number;
disabled?: boolean;
reading?: boolean;
onPick: (files: File[]) => Promise<void>;
className?: string;
inputRef?: RefObject<HTMLInputElement | null>;
}) {
const inputRef = useRef<HTMLInputElement>(null);
const fallbackRef = useRef<HTMLInputElement>(null);
const ref = inputRef ?? fallbackRef;

return (
<>
<input
ref={inputRef}
ref={ref}
type="file"
accept={acceptedImageTypes.join(',')}
multiple
Expand All @@ -146,7 +150,7 @@ export function ImagePickerButton({
variant="ghost"
size="icon"
disabled={disabled || reading || count >= maxImages}
onClick={() => inputRef.current?.click()}
onClick={() => ref.current?.click()}
aria-label={reading ? 'Reading images' : 'Attach images'}
aria-busy={reading}
className={cn('text-muted-foreground', className)}
Expand Down
16 changes: 14 additions & 2 deletions resources/js/components/commit-quote-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ function QuoteComposer({
);
const linkCard = useLinkCard(body);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const imageInputRef = useRef<HTMLInputElement>(null);
const commitRules = useCommitRules();
const keys = useKeyLabels();

Expand Down Expand Up @@ -174,7 +175,7 @@ function QuoteComposer({
onEscapeKeyDown={(event) => {
if (
document.querySelector(
'[data-mention-suggestions], [role="dialog"].yarl__root',
'[data-mention-suggestions], [data-slash-suggestions], [data-emoji-suggestions], [role="dialog"].yarl__root',
) ||
(typeof Element !== 'undefined' &&
event.target instanceof Element &&
Expand All @@ -188,7 +189,7 @@ function QuoteComposer({
typeof Element !== 'undefined' &&
event.target instanceof Element &&
event.target.closest(
'[data-mention-suggestions], [role="dialog"].yarl__root',
'[data-mention-suggestions], [data-slash-suggestions], [data-emoji-suggestions], [role="dialog"].yarl__root',
)
) {
event.preventDefault();
Expand Down Expand Up @@ -265,6 +266,16 @@ function QuoteComposer({
value={body}
onValueChange={updateBody}
onKeyDown={submitOnModEnter}
onImage={() =>
imageInputRef.current?.click()
}
commandContext={{
surface: 'quote',
hasPoll: false,
imageCount: images.length,
isScheduling: false,
processing,
}}
onPaste={attachments.onPaste}
onFocus={
isRestored ? moveCaretToEnd : undefined
Expand Down Expand Up @@ -333,6 +344,7 @@ function QuoteComposer({
disabled={processing}
reading={attachments.reading}
onPick={attachments.pick}
inputRef={imageInputRef}
className="size-8"
/>
<PreviewToggle
Expand Down
10 changes: 10 additions & 0 deletions resources/js/components/commit-thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,7 @@ function ReplyComposer({
reply_recipients: [],
});
const textareaRef = useRef<HTMLTextAreaElement>(null);
const imageInputRef = useRef<HTMLInputElement>(null);
const commitRules = useCommitRules();
const isReplyable =
(images.length > 0 || characterCount(body.trim()) >= minBodyLength) &&
Expand Down Expand Up @@ -1562,6 +1563,14 @@ function ReplyComposer({
value={body}
onValueChange={updateBody}
onKeyDown={onKeyDown}
onImage={() => imageInputRef.current?.click()}
commandContext={{
surface: 'reply',
hasPoll: false,
imageCount: images.length,
isScheduling: false,
processing,
}}
onPaste={attachments.onPaste}
onFocus={isRestored ? moveCaretToEnd : undefined}
maxLength={maxRawBodyLength}
Expand Down Expand Up @@ -1640,6 +1649,7 @@ function ReplyComposer({
disabled={processing}
reading={attachments.reading}
onPick={attachments.pick}
inputRef={imageInputRef}
className="size-8"
/>
<PreviewToggle preview={preview} className="-ml-2 size-8" />
Expand Down
Loading