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 backend/src/__tests__/oauth-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('trusted OAuth client configuration', () => {
expect(trustedOAuthClients()).toEqual([
{
clientId: 'writing-tools-mindmap',
name: 'Writing Tools Mindmap',
name: 'Mindmap',
redirectUris: [PROD_REDIRECT],
},
]);
Expand Down
10 changes: 10 additions & 0 deletions backend/src/__tests__/oauth-provider.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,21 @@ describe.sequential('OAuth provider (Mindmap client)', () => {
const html = await login.text();
expect(html).toContain('Sign in with Google');
expect(html).toContain('oauth_query');
// Names the requesting trusted client, so the user knows what they're connecting.
expect(loginUrl.searchParams.get('client_id')).toBe(CLIENT_ID);
expect(html).toContain('<strong>Mindmap</strong> wants to use Thoughtful');
// The resource is part of the signed continuation, not re-added by the page.
expect(loginUrl.searchParams.get('resource')).toBe(ORIGIN);
expect(loginUrl.searchParams.getAll('ba_param')).toContain('resource');
});

it('falls back to a generic login prompt for an unknown client_id', async () => {
const login = await app.request(`${ORIGIN}/api/oauth/login?client_id=not-a-client`);
const html = await login.text();
expect(html).toContain('the app you came from');
expect(html).not.toContain('<strong>');
});

it('resumes authorization from the login page once sign-in sets a session', async () => {
// The provider's continuation fires on any sign-in that sets a session cookie,
// so anonymous sign-in stands in for the Google callback here.
Expand Down
2 changes: 1 addition & 1 deletion backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const trustedOAuthClients = (): TrustedOAuthClient[] =>
[
{
clientId: envString('MINDMAP_OAUTH_CLIENT_ID'),
name: 'Writing Tools Mindmap',
name: 'Mindmap',
redirectUris: envList('MINDMAP_OAUTH_REDIRECT_URIS'),
},
].filter((client) => client.clientId && client.redirectUris.length > 0);
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const DEBUG_HTML = `<!DOCTYPE html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Auth Debug — Writing Tools Backend</title>
<title>Auth Debug — Thoughtful backend</title>
<style>
body { font-family: sans-serif; max-width: 560px; margin: 2.5rem auto; padding: 0 1.5rem; line-height: 1.5; }
h1 { font-size: 1.3rem; }
Expand Down
6 changes: 3 additions & 3 deletions backend/src/routes/device-approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import type { Context } from 'hono';
// Registered whenever auth is enabled (see index.ts).

// User-facing product name, in one place so it's tunable. Interpolated into the
// template below (static HTML and the inline script alike). Cross-app naming
// (manifest DisplayName, frontend) is a broader cleanup tracked separately.
const PRODUCT_NAME = 'Thoughtful AI Tools';
// template below (static HTML and the inline script alike). Keep it in step with
// the OAuth login page (oauth-login.ts) and the add-in's own branding.
const PRODUCT_NAME = 'Thoughtful';

const DEVICE_HTML = `<!DOCTYPE html>
<html lang="en">
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/device-taskpane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function buildDebugDeviceHtml(serializedClientId: string) {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Device Flow Debug — Writing Tools Backend</title>
<title>Device Flow Debug — Thoughtful backend</title>
<style>
body { font-family: sans-serif; max-width: 560px; margin: 2.5rem auto; padding: 0 1.5rem; line-height: 1.5; }
h1 { font-size: 1.3rem; }
Expand Down
28 changes: 20 additions & 8 deletions backend/src/routes/oauth-login.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// The OAuth provider's login page, shared by every trusted client: nothing on it
// depends on which client sent the user here. The provider only redirects here
// when there's no session, so the page is one button.
// The OAuth provider's login page, shared by every trusted client. The only thing
// that depends on which client sent the user here is its display name, looked up
// from the trusted-client list by the query's client_id so an unknown id can't put
// arbitrary text on the page. The provider only redirects here when there's no
// session, so the page is one button.
//
// It hands the provider's signed authorization query back as `oauth_query` on
// sign-in. The provider keeps it in the Google OAuth state, and once the callback
Expand All @@ -9,22 +11,24 @@
// Only the signed parameters are sent (the subset ba_param names, plus the
// signature), matching what the provider's own client plugin does.

const LOGIN_HTML = `<!doctype html>
import { trustedOAuthClients } from '../config.js';

const loginHtml = (lede: string) => `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sign in to Writing Tools</title>
<title>Sign in to Thoughtful</title>
<style>
body{font:16px/1.5 system-ui,sans-serif;max-width:620px;margin:3rem auto;padding:0 1.5rem;color:#172033}
h1{font-size:1.5rem}button{font:inherit;padding:.65rem 1rem;border:1px solid #3157d5;border-radius:7px;cursor:pointer;background:#3157d5;color:white}
.error{color:#b42318}
</style>
</head>
<body>
<h1>Sign in to Writing Tools</h1>
<h1>Sign in to Thoughtful</h1>
<main id="app">
<p>Sign in to let the app you came from use Writing Tools AI on your behalf.</p>
<p>${lede}</p>
<button id="sign-in">Sign in with Google</button>
</main>
<script>
Expand All @@ -45,6 +49,14 @@ const LOGIN_HTML = `<!doctype html>
</body>
</html>`;

const escapeHtml = (text: string): string =>
text.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);

export function oauthLoginHandler(c: import('hono').Context): Response {
return c.html(LOGIN_HTML);
const clientId = c.req.query('client_id');
const client = trustedOAuthClients().find((entry) => entry.clientId === clientId);
const lede = client
? `<strong>${escapeHtml(client.name)}</strong> wants to use Thoughtful's AI on your behalf. Sign in to connect it.`
: 'Sign in to let the app you came from use Thoughtful on your behalf.';
return c.html(loginHtml(lede));
}
11 changes: 7 additions & 4 deletions docs/oauth-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The backend is an OAuth 2.1 authorization server (Better Auth's `oauthProvider`
plugin under `/api/auth/oauth2/*`), and its OpenAI proxy is the resource server
that accepts the resulting access tokens. This lets separately hosted
first-party apps call the proxy as a signed-in Writing Tools user.
first-party apps call the proxy as a signed-in Thoughtful user.

The first and only client is the standalone Mindmap. It may call the two
text-generation proxies; it receives no document or room access, and the user
Expand Down Expand Up @@ -41,8 +41,11 @@ not register localhost.
## Login

When authorize finds no session, the provider redirects to `/api/oauth/login`
with a signed copy of the authorization query. That page is one "Sign in with
Google" button: it posts the signed query as `oauth_query` with the social
with a signed copy of the authorization query. That page names the requesting
client ("**Mindmap** wants to use Thoughtful's AI on your behalf"), looking up
`client_id` in the trusted-client list so only configured names can appear; an
unknown id gets a generic prompt. Since trusted clients skip consent, a user who
is already signed in never sees this page. It is one "Sign in with Google" button: it posts the signed query as `oauth_query` with the social
sign-in, the provider carries it through the Google OAuth state, and after the
callback sets the session it re-runs authorize and redirects to the client with
a code. A query edited after signing is refused (`invalid_signature`).
Expand Down Expand Up @@ -71,7 +74,7 @@ introspection is configured, so opaque access tokens are never accepted.

Revocation rides on the authorizing sign-in: the token's `sid` claim names the
Better Auth session that approved it, and the proxy requires that session to
still exist, be unexpired, and belong to `sub`. Signing out of Writing Tools (or
still exist, be unexpired, and belong to `sub`. Signing out of Thoughtful (or
the session expiring) therefore revokes Mindmap's token immediately. Disabling
the client, by contrast, only stops new grants.

Expand Down
4 changes: 2 additions & 2 deletions frontend/public/longDescription.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<link
href="https://fonts.googleapis.com/css2?family=Albert+Sans:ital,wght@0,100..900;1,100..900&family=Fredoka:[email protected]&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet">
<title>AI Writing Tools Add-in</title>
<title>Thoughtful Add-in</title>
<style>
body {
font-family: "Plus Jakarta Sans", sans-serif;
Expand Down Expand Up @@ -45,7 +45,7 @@
</head>
<body>
<article id="main-article">
<h1>Thoughtful Writing Tools Add-in</h1>
<h1>Thoughtful</h1>
<p>Thoughtful helps you write more thoughtfully. Its AI encourages your own thinking.</p>
<section class="article-section">
<h2>Key Features</h2>
Expand Down
12 changes: 6 additions & 6 deletions google-docs-addon/Code.gs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

/**
* Thoughtful AI Tools - Google Docs Add-on
* Thoughtful - Google Docs Add-on
*
* This Apps Script bridges the sidebar UI (HTML/JS/React) and the Google Docs
* document (via DocumentApp), providing document operations only. The React app
Expand All @@ -26,7 +26,7 @@
function onOpen(e) {
DocumentApp.getUi()
.createAddonMenu()
.addItem('Open Thoughtful AI Tools', 'showSidebar')
.addItem('Open Thoughtful', 'showSidebar')
.addToUi();
}

Expand Down Expand Up @@ -56,12 +56,12 @@ function onFileScopeGranted(e) {
*/
function createHomepageCard() {
const card = CardService.newCardBuilder()
.setHeader(CardService.newCardHeader().setTitle('Thoughtful AI Tools'))
.setHeader(CardService.newCardHeader().setTitle('Thoughtful'))
.addSection(
CardService.newCardSection()
.addWidget(
CardService.newTextParagraph()
.setText('Click the button below to open Thoughtful AI Tools in a sidebar for the full experience.')
.setText('Click the button below to open Thoughtful in a sidebar for the full experience.')
)
.addWidget(
CardService.newTextButton()
Expand Down Expand Up @@ -97,11 +97,11 @@ function createHomepageCard() {
}

/**
* Shows the sidebar with the Thoughtful AI Tools UI.
* Shows the sidebar with the Thoughtful UI.
*/
function showSidebar() {
const html = HtmlService.createHtmlOutputFromFile('sidebar')
.setTitle('Thoughtful AI Tools')
.setTitle('Thoughtful')
.setWidth(400);
DocumentApp.getUi().showSidebar(html);
}
Expand Down
12 changes: 6 additions & 6 deletions google-docs-addon/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Google Docs Add-on

This folder contains the Google Apps Script code for the Writing Tools Google Docs add-on.
This folder contains the Google Apps Script code for the Thoughtful Google Docs add-on.

## Architecture

Expand Down Expand Up @@ -76,14 +76,14 @@ operations. All backend traffic (chat, revise, logging) goes from the React app
```bash
cd google-docs-addon
rm .clasp.json
clasp create --type docs --title "Writing Tools"
clasp create --type docs --title "Thoughtful"
```
Google may show another authorization screen — click **Allow**.
This creates a new `.clasp.json` with your own `scriptId`.

> **Alternative (if `clasp create` still gives errors):**
> 1. Go to https://script.google.com and click **New project**
> 2. Rename it to "Writing Tools"
> 2. Rename it to "Thoughtful"
> 3. Copy the script ID from the URL: `https://script.google.com/home/projects/<SCRIPT_ID>/edit`
> 4. Create `.clasp.json` manually:
> ```json
Expand All @@ -102,12 +102,12 @@ operations. All backend traffic (chat, revise, logging) goes from the React app
> **If `clasp open` is not recognized:**
> - Make sure clasp is installed globally: `npm install -g @google/clasp`
> - On Windows, ensure the global npm bin folder (`%APPDATA%\npm`) is in your PATH
> - **Alternative:** Open https://script.google.com directly and find your "Writing Tools" project, or hover over it in the project list and click **Open Document**
> - **Alternative:** Open https://script.google.com directly and find your "Thoughtful" project, or hover over it in the project list and click **Open Document**

In the Apps Script editor: **Deploy → Test deployments → + Add test** → select a Google Doc → **Save**

7. **First time opening the sidebar in Google Docs:**
Go to **Extensions → Writing Tools → Open Writing Tools**.
Go to **Extensions → Thoughtful → Open Thoughtful**.
Google will show an authorization screen — click **Review Permissions** → select your Google account → click **Allow**.
This only happens once.

Expand All @@ -120,7 +120,7 @@ npm run dev-server
```

Wait for Vite to print its local URL, then open your Google Doc and go to:
**Extensions → Writing Tools → Open Writing Tools**
**Extensions → Thoughtful → Open Thoughtful**

No tunneling/ngrok is needed — the sidebar loads straight from localhost, and
the dev server proxies `/api` to the local backend.
Expand Down
2 changes: 1 addition & 1 deletion google-docs-addon/appsscript.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
],
"addOns": {
"common": {
"name": "Writing Tools",
"name": "Thoughtful",
"logoUrl": "https://app.thoughtful-ai.com/assets/logo_black.png",
"homepageTrigger": { "runFunction": "onHomepage" }
},
Expand Down
10 changes: 5 additions & 5 deletions google-docs-addon/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Writing Tools</title>
<title>Thoughtful</title>

<style>
/* Base styles while loading */
Expand Down Expand Up @@ -56,7 +56,7 @@
<div id="root">
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">Loading Writing Tools...</div>
<div class="loading-text">Loading Thoughtful...</div>
</div>
</div>

Expand Down Expand Up @@ -211,7 +211,7 @@
function loadFrom(base) {
document.getElementById('root').innerHTML =
'<div class="loading-container"><div class="loading-spinner"></div>' +
'<div class="loading-text">Loading Writing Tools…</div></div>';
'<div class="loading-text">Loading Thoughtful…</div></div>';

var link = document.createElement('link');
link.rel = 'stylesheet';
Expand All @@ -222,7 +222,7 @@
s.src = base + '/google-docs.bundle.js';
s.onerror = function () {
document.getElementById('root').innerHTML =
'<div class="error-container"><h3>Could not load Writing Tools</h3>' +
'<div class="error-container"><h3>Could not load Thoughtful</h3>' +
'<p>Failed to load the app from <code>' + base + '</code>.</p></div>';
};
document.body.appendChild(s);
Expand All @@ -232,7 +232,7 @@
var root = document.getElementById('root');
root.innerHTML =
'<div style="padding:20px;color:#202124;">' +
'<p style="margin:0 0 12px;font-weight:600;">Dev mode — load Writing Tools from:</p>' +
'<p style="margin:0 0 12px;font-weight:600;">Dev mode — load Thoughtful from:</p>' +
'<div id="srcButtons"></div></div>';
var box = document.getElementById('srcButtons');
Object.keys(SOURCES).forEach(function (key) {
Expand Down
6 changes: 3 additions & 3 deletions google-docs-addon/store-listing.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# Application Name

Thoughtful AI Tools
Thoughtful

# Short Description

AI helps you think and reflect while writing, not write for you.

# Detailed Description

Thoughtful AI Tools helps you write thoughtfully by keeping you fully engaged with your own ideas. Instead of generating text for you, it helps you see your document — its structure, main points, concepts, and gaps — so you can decide what to work on next.
Thoughtful helps you write thoughtfully by keeping you fully engaged with your own ideas. Instead of generating text for you, it helps you see your document — its structure, main points, concepts, and gaps — so you can decide what to work on next.

Open the sidebar in any document and use it to:

Visualize your document — generate hierarchical outlines, possible structures, and exemplar-based outlines to understand how your writing is organized.
Analyze your content — surface main points, key concepts, claims and arguments, potential counterarguments, and the questions a reader might ask.
Find where to work next — get a prioritized list of places in your draft that could use attention, each linked directly to the relevant text.
Jump across tabs — select a word or phrase and instantly find and navigate to where it appears in your document's other tabs.
Every suggestion links back to specific parts of your document, so you stay grounded in your own writing. Thoughtful AI Tools is designed to support thinking and reflection — you remain the author.
Every suggestion links back to specific parts of your document, so you stay grounded in your own writing. Thoughtful is designed to support thinking and reflection — you remain the author.
Loading