refactor: rename workspace branding and update project overview
This commit is contained in:
@@ -1,54 +1,67 @@
|
|||||||
# Twitter Lite
|
# Personal Workspace
|
||||||
|
|
||||||
Twitter Lite is a personal research deck for Twitter and Mastodon. Create
|
A personal workspace for understanding the day, choosing the next action,
|
||||||
multiple named decks and arrange up to six columns per deck. Each column is
|
keeping context, and exploring information. Home brings together a short brief,
|
||||||
bound to a connection account, so platforms and multiple accounts work side by side.
|
flexible tasks and routines, messages, notes, and reading. A shared sidebar opens
|
||||||
|
each dedicated workspace; its bottom avatar manages connected accounts.
|
||||||
|
|
||||||
## Scope
|
The interface is in English. User-entered text and external posts retain their
|
||||||
|
original language.
|
||||||
|
|
||||||
- Multiple deck profiles with creation, selection, renaming, and deletion
|
## Workspaces and current status
|
||||||
- Twitter search, user timeline, and list columns with independent relay accounts
|
|
||||||
|
| Workspace | Route | Current implementation |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Home | `/` | State-derived daily brief, flexible tasks and routines, and natural-language action proposals. Local prototype. |
|
||||||
|
| Messages | `/support` | Sample conversations, editable replies, scheduling previews, and deferral. Sending is simulated. |
|
||||||
|
| Journal | `/journal` | Notes, resume points, medication and weight entries, append-only corrections, and summary previews. Local prototype. |
|
||||||
|
| Reader | `/inbox` | Searchable sample articles and a responsive article reader. Recommendation from activity history is not connected yet. |
|
||||||
|
| Vitals | `/vitals` | Body and Environment dashboards with sample readings, charts, and simulated controls. No health or device connection. |
|
||||||
|
| Research | `/deck` | Twitter and Mastodon columns, connected accounts, persistent decks, and optional Codex research chat. |
|
||||||
|
|
||||||
|
Home, Messages, and Journal share in-memory state across navigation; reloading
|
||||||
|
resets it. The home input uses a deterministic English/Japanese interpreter to
|
||||||
|
preview an action before confirmation, rather than an LLM. Research decks,
|
||||||
|
connections, and chat history use server-side persistence.
|
||||||
|
|
||||||
|
See [execution support](docs/execution-support.md) and the
|
||||||
|
[vitals preview](docs/vitals-preview.md) for prototype scope.
|
||||||
|
|
||||||
|
## Direction: personal context and execution
|
||||||
|
|
||||||
|
The intended connection with the separate private `life` / Rensheng repository
|
||||||
|
is a loop: read relevant personal context, suggest useful next actions, record
|
||||||
|
what happened, and compile meaningful outcomes back into durable context.
|
||||||
|
This integration is planned; the app does not currently read or update life.
|
||||||
|
|
||||||
|
Rensheng would retain personal context, preferences, and the background of
|
||||||
|
ongoing matters. This workspace would own its execution state and interaction
|
||||||
|
history, with original conversations and appointments remaining in their source
|
||||||
|
systems. Context should carry source references and verification dates.
|
||||||
|
|
||||||
|
Interests are queries over activity history, not settings the user must maintain.
|
||||||
|
Routines provide optional guidance rather than a daily completion quota. Home
|
||||||
|
should help the user choose a manageable next step, with room to defer, dismiss,
|
||||||
|
or change direction.
|
||||||
|
|
||||||
|
## Research capabilities
|
||||||
|
|
||||||
|
- Multiple named decks with up to six columns each
|
||||||
|
- Twitter search, user timelines, and lists with independent relay accounts
|
||||||
- Mastodon OAuth, multiple accounts, search, user, list, and hashtag columns
|
- Mastodon OAuth, multiple accounts, search, user, list, and hashtag columns
|
||||||
- Native X search syntax, Top/Latest ranking, and optional `filter:follows`
|
- Native X search syntax, Top/Latest ranking, and optional `filter:follows`
|
||||||
- List discovery for the profile selected in the column editor
|
- List discovery for the account selected in the column editor
|
||||||
- Column editing, ordering, deletion/undo, manual refresh, and cursor pagination
|
- Column editing, ordering, deletion/undo, refresh, and cursor pagination
|
||||||
- Platform logos in column headers distinguish Twitter and Mastodon at a glance
|
- SQLite-backed shared decks, revision conflicts, and device-local selection
|
||||||
- SQLite-backed shared decks, revision conflicts, and device-local active selection
|
- Temporary views for exploration, with explicit saving
|
||||||
- Temporary views for AI exploration, with explicit save and temporary copies
|
|
||||||
- Server-only encrypted Mastodon credentials and configurable Tailscale owner access
|
- Server-only encrypted Mastodon credentials and configurable Tailscale owner access
|
||||||
- Read-only cards with original-post links, text, media, and quotes
|
- Read-only post cards with source links, text, media, and quotes
|
||||||
- Experimental WebMCP tools to manage decks and read or paginate their columns
|
- Experimental WebMCP tools to manage decks and read their columns
|
||||||
- Prototype: chat with a resident home Codex beside a live deck, reuse existing
|
- Optional resident Codex research chat, cited host-side Markdown reports,
|
||||||
decks and write cited Markdown research reports on the host
|
and saved conversations with deck, account, and citation restoration
|
||||||
- Saved chat history with deck, account and citation restoration across devices
|
|
||||||
and backend restarts
|
|
||||||
|
|
||||||
The system interface is in English; user-entered text and external posts retain their original language.
|
Original-post links open their source site; SNS reply threads are not rendered
|
||||||
|
inside the app. Bluesky, Threads, and Nostr connectors are not implemented.
|
||||||
`/` opens the daily home with a greeting, natural-language action proposals,
|
|
||||||
flexible tasks/routines, messages, notes, and reading. `/deck` opens Twitter Lite.
|
|
||||||
The shared sidebar also opens
|
|
||||||
the article reader at `/inbox`. This UI-only prototype shows articles inferred
|
|
||||||
from past reading, searches, and research, with no manual interest settings.
|
|
||||||
Articles are fictional samples; search and article selection work locally.
|
|
||||||
On mobile, the article list and reader switch views. The shared sidebar’s bottom
|
|
||||||
avatar opens connected-account management from either workspace.
|
|
||||||
|
|
||||||
The shared sidebar also contains `/support` (communication support) and `/journal`
|
|
||||||
(notes and health records). These use fictional conversations and local session
|
|
||||||
state to preview reply review, scheduling, deferral, recording, and corrections.
|
|
||||||
See [execution support](docs/execution-support.md) for scope and limitations.
|
|
||||||
|
|
||||||
`/vitals` is a separate [health/environment reference preview](docs/vitals-preview.md)
|
|
||||||
with sample scores, body systems, environmental charts, and simulated controls.
|
|
||||||
|
|
||||||
The former SNS reader, search,
|
|
||||||
list, user-profile, and conversation routes have been removed. Original-post
|
|
||||||
links on post cards open their source site; SNS reply threads are not rendered
|
|
||||||
inside the app.
|
|
||||||
|
|
||||||
Bluesky/Threads/Nostr connectors are not implemented yet. An external browser
|
|
||||||
agent can also create and read decks through WebMCP.
|
|
||||||
|
|
||||||
## Requirements and setup
|
## Requirements and setup
|
||||||
|
|
||||||
@@ -129,10 +142,10 @@ accounts' lists before planning new searches, then read relevant list columns
|
|||||||
for evidence. Follow-ups can reuse catalogs already fetched in the conversation.
|
for evidence. Follow-ups can reuse catalogs already fetched in the conversation.
|
||||||
Manual deck creation and column editing remain
|
Manual deck creation and column editing remain
|
||||||
available. The backend handles tools even when the browser is closed.
|
available. The backend handles tools even when the browser is closed.
|
||||||
Use **新しいチャット** to leave the current conversation and start a fresh Codex
|
Use **New chat** to leave the current conversation and start a fresh Codex
|
||||||
thread with your next message. The open deck and host reports remain available.
|
thread with your next message. The open deck and host reports remain available.
|
||||||
If a turn is running, stop it first. The conversation reset is shared across
|
If a turn is running, stop it first. The conversation reset is shared across
|
||||||
open devices. **チャット履歴** reopens saved conversations with their account
|
open devices. **Chat history** reopens saved conversations with their account
|
||||||
selection, deck and citations; the next message resumes the same Codex thread.
|
selection, deck and citations; the next message resumes the same Codex thread.
|
||||||
SQLite stores conversation snapshots and the active conversation selection.
|
SQLite stores conversation snapshots and the active conversation selection.
|
||||||
Each turn is limited to 6 columns, 12 upstream requests shared between list
|
Each turn is limited to 6 columns, 12 upstream requests shared between list
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Execution support prototype
|
# Execution support prototype
|
||||||
|
|
||||||
The shared sidebar opens `/support` (Messages) and `/journal` (Journal), alongside
|
The shared sidebar opens `/support` (Messages) and `/journal` (Journal), alongside
|
||||||
Reader and Twitter Lite. These are UI-only prototypes with fictional contacts
|
Reader and Research. These are UI-only prototypes with fictional contacts
|
||||||
and in-memory state. Reloading resets the prototype state. Tasks, communication status, outcomes, and
|
and in-memory state. Reloading resets the prototype state. Tasks, communication status, outcomes, and
|
||||||
journal entries are shared between routes for the current browser session.
|
journal entries are shared between routes for the current browser session.
|
||||||
No messages, calendar events, medical records, or Rensheng pages are written.
|
No messages, calendar events, medical records, or Rensheng pages are written.
|
||||||
@@ -41,7 +41,7 @@ Start here, Routines, and If there’s room. There are no fixed time slots.
|
|||||||
Tasks can be reordered, completed, deferred, restored, or followed as routine
|
Tasks can be reordered, completed, deferred, restored, or followed as routine
|
||||||
steps. Communications can be reviewed directly in a home dialog; changes also
|
steps. Communications can be reviewed directly in a home dialog; changes also
|
||||||
appear in `/support`. Journal entries and corrections appear on the home, and
|
appear in `/support`. Journal entries and corrections appear on the home, and
|
||||||
reader samples open for reading. Twitter Lite remains a separate work surface.
|
reader samples open for reading. Research remains a separate work surface.
|
||||||
|
|
||||||
The input currently uses a deterministic English/Japanese mock interpreter, not an LLM.
|
The input currently uses a deterministic English/Japanese mock interpreter, not an LLM.
|
||||||
It proposes typed actions for task creation/status/reordering, routine steps,
|
It proposes typed actions for task creation/status/reordering, routine steps,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
`/deck` renders the research workspace; `/` is the integrated daily home. A collapsible shadcn/ui
|
`/deck` renders the research workspace; `/` is the integrated daily home. A collapsible shadcn/ui
|
||||||
sidebar based on `sidebar-09` shares its navigation rail with the article reader
|
sidebar based on `sidebar-09` shares its navigation rail with the article reader
|
||||||
at `/inbox`. Within Twitter Lite, it switches between research chat and deck management.
|
at `/inbox`. Within Research, it switches between research chat and deck management.
|
||||||
Deck management switches decks and adds columns. The avatar at the bottom of the
|
Deck management switches decks and adds columns. The avatar at the bottom of the
|
||||||
shared sidebar opens connected-account management in a dialog from either workspace.
|
shared sidebar opens connected-account management in a dialog from either workspace.
|
||||||
Up to six ordered columns scroll independently; only the active deck
|
Up to six ordered columns scroll independently; only the active deck
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Shared storage and Mastodon OAuth
|
# Shared storage and Mastodon OAuth
|
||||||
|
|
||||||
Twitter Lite runs as a single personal server behind Tailscale Serve. The
|
Personal Workspace runs as a single personal server behind Tailscale Serve. The
|
||||||
backend binds to loopback and accepts only the configured Tailscale login.
|
backend binds to loopback and accepts only the configured Tailscale login.
|
||||||
Browser requests that change state must have the configured Origin. OAuth
|
Browser requests that change state must have the configured Origin. OAuth
|
||||||
callbacks also pass the owner check; the browser must be able to reach the
|
callbacks also pass the owner check; the browser must be able to reach the
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ test('selects articles and keeps the reader focused on their content', async ({
|
|||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('recovers from empty search and shares navigation with Twitter Lite', async ({
|
test('recovers from empty search and shares navigation with Research', async ({
|
||||||
page,
|
page,
|
||||||
a11y,
|
a11y,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -61,7 +61,7 @@ test('recovers from empty search and shares navigation with Twitter Lite', async
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Edit interests', exact: true }),
|
page.getByRole('button', { name: 'Edit interests', exact: true }),
|
||||||
).toHaveCount(0)
|
).toHaveCount(0)
|
||||||
await page.getByRole('link', { name: 'Twitter Lite', exact: true }).click()
|
await page.getByRole('link', { name: 'Research', exact: true }).click()
|
||||||
await expect(page).toHaveURL(/\/deck$/)
|
await expect(page).toHaveURL(/\/deck$/)
|
||||||
await page.getByRole('link', { name: 'Reader', exact: true }).click()
|
await page.getByRole('link', { name: 'Reader', exact: true }).click()
|
||||||
await expect(page).toHaveURL(/\/inbox$/)
|
await expect(page).toHaveURL(/\/inbox$/)
|
||||||
|
|||||||
@@ -100,13 +100,13 @@
|
|||||||
let cfg = config.services.twitter-lite;
|
let cfg = config.services.twitter-lite;
|
||||||
in {
|
in {
|
||||||
options.services.twitter-lite = {
|
options.services.twitter-lite = {
|
||||||
enable = lib.mkEnableOption "Twitter Lite";
|
enable = lib.mkEnableOption "Personal Workspace";
|
||||||
|
|
||||||
package = lib.mkOption {
|
package = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = mkTwitterLite pkgs;
|
default = mkTwitterLite pkgs;
|
||||||
defaultText = lib.literalExpression "twitter-lite.packages.\${pkgs.system}.default";
|
defaultText = lib.literalExpression "twitter-lite.packages.\${pkgs.system}.default";
|
||||||
description = "Twitter Lite package to run.";
|
description = "Personal Workspace package to run.";
|
||||||
};
|
};
|
||||||
|
|
||||||
relayBaseUrl = lib.mkOption {
|
relayBaseUrl = lib.mkOption {
|
||||||
@@ -143,13 +143,13 @@
|
|||||||
port = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
default = 3000;
|
default = 3000;
|
||||||
description = "Port on which Twitter Lite listens.";
|
description = "Port on which Personal Workspace listens.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
systemd.services.twitter-lite = {
|
systemd.services.twitter-lite = {
|
||||||
description = "Twitter Lite";
|
description = "Personal Workspace";
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
environment = {
|
environment = {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ function Workspace({
|
|||||||
<SidebarInset className="min-w-0 overflow-hidden">
|
<SidebarInset className="min-w-0 overflow-hidden">
|
||||||
<header className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
|
<header className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
|
||||||
<SidebarTrigger aria-label="Toggle sidebar" />
|
<SidebarTrigger aria-label="Toggle sidebar" />
|
||||||
<span className="text-sm font-medium">Twitter Lite</span>
|
<span className="text-sm font-medium">Research</span>
|
||||||
<span className="ml-auto text-xs text-muted-foreground">
|
<span className="ml-auto text-xs text-muted-foreground">
|
||||||
Research workspace
|
Research workspace
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function WorkspaceNavigation({
|
|||||||
active: pathname === '/inbox',
|
active: pathname === '/inbox',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Twitter Lite',
|
title: 'Research',
|
||||||
to: '/deck',
|
to: '/deck',
|
||||||
icon: Search,
|
icon: Search,
|
||||||
active: pathname === '/deck',
|
active: pathname === '/deck',
|
||||||
|
|||||||
@@ -548,7 +548,7 @@ export function HomePage() {
|
|||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
<Link to="/deck" className="home-deck-link">
|
<Link to="/deck" className="home-deck-link">
|
||||||
Explore in Twitter Lite
|
Explore in Research
|
||||||
<ArrowUpRight className="size-4" />
|
<ArrowUpRight className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ describe('home natural-language proposals', () => {
|
|||||||
['連絡を開いて', '/support'],
|
['連絡を開いて', '/support'],
|
||||||
['記録', '/journal'],
|
['記録', '/journal'],
|
||||||
['リーダーを開く', '/inbox'],
|
['リーダーを開く', '/inbox'],
|
||||||
['Twitter Liteに移動', '/deck'],
|
['Researchに移動', '/deck'],
|
||||||
])('recognizes navigation: %s', (input, to) => {
|
])('recognizes navigation: %s', (input, to) => {
|
||||||
expect(interpretAction(input, tasks, contacts)).toMatchObject({
|
expect(interpretAction(input, tasks, contacts)).toMatchObject({
|
||||||
action: { type: 'navigate', to },
|
action: { type: 'navigate', to },
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ export function interpretAction(
|
|||||||
return { error: 'Please review this action in the relevant screen.' }
|
return { error: 'Please review this action in the relevant screen.' }
|
||||||
text = text
|
text = text
|
||||||
.replace(
|
.replace(
|
||||||
/^open (messages|journal|reader|twitter lite|deck)$/i,
|
/^open (messages|journal|reader|research|deck)$/i,
|
||||||
(_, name: string) =>
|
(_, name: string) =>
|
||||||
({
|
({
|
||||||
messages: '連絡',
|
messages: '連絡',
|
||||||
journal: '記録',
|
journal: '記録',
|
||||||
reader: 'リーダー',
|
reader: 'リーダー',
|
||||||
'twitter lite': 'Twitter Lite',
|
research: 'Research',
|
||||||
deck: 'デッキ',
|
deck: 'デッキ',
|
||||||
})[name.toLowerCase()] ?? name,
|
})[name.toLowerCase()] ?? name,
|
||||||
)
|
)
|
||||||
@@ -83,7 +83,7 @@ export function interpretAction(
|
|||||||
連絡: '/support',
|
連絡: '/support',
|
||||||
記録: '/journal',
|
記録: '/journal',
|
||||||
リーダー: '/inbox',
|
リーダー: '/inbox',
|
||||||
'Twitter Lite': '/deck',
|
Research: '/deck',
|
||||||
デッキ: '/deck',
|
デッキ: '/deck',
|
||||||
}
|
}
|
||||||
const destination = text.replace(/(?:を開いて|を開く|に移動)$/, '')
|
const destination = text.replace(/(?:を開いて|を開く|に移動)$/, '')
|
||||||
@@ -93,7 +93,7 @@ export function interpretAction(
|
|||||||
if (route)
|
if (route)
|
||||||
return {
|
return {
|
||||||
action: { type: 'navigate', to: route },
|
action: { type: 'navigate', to: route },
|
||||||
label: `Open ${{ '/support': 'Messages', '/journal': 'Journal', '/inbox': 'Reader', '/deck': 'Twitter Lite' }[route]}`,
|
label: `Open ${{ '/support': 'Messages', '/journal': 'Journal', '/inbox': 'Reader', '/deck': 'Research' }[route]}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
const note = text.match(/^(メモ|再開地点)[::\s]+(.+)$/s)
|
const note = text.match(/^(メモ|再開地点)[::\s]+(.+)$/s)
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ async function getApp(origin: string, database: AppDatabase, send: Send) {
|
|||||||
const registered = appSchema.parse(
|
const registered = appSchema.parse(
|
||||||
await (
|
await (
|
||||||
await formRequest(send, origin, '/api/v1/apps', {
|
await formRequest(send, origin, '/api/v1/apps', {
|
||||||
client_name: 'Twitter Lite',
|
client_name: 'Personal Workspace',
|
||||||
redirect_uris: redirectUri,
|
redirect_uris: redirectUri,
|
||||||
scopes,
|
scopes,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||||||
content: 'width=device-width, initial-scale=1',
|
content: 'width=device-width, initial-scale=1',
|
||||||
},
|
},
|
||||||
{ name: 'referrer', content: 'no-referrer' },
|
{ name: 'referrer', content: 'no-referrer' },
|
||||||
{ title: 'Twitter Lite' },
|
{ title: 'Personal Workspace' },
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
{ rel: 'stylesheet', href: appCss },
|
{ rel: 'stylesheet', href: appCss },
|
||||||
|
|||||||
Reference in New Issue
Block a user